python3.6+gcc-11 でコンパイルが失敗する件
はじめに
ネバ―・フレンズ・Tです。こんにちわ。
大人の事情により、新しいLinux環境でどうしても古いpythonを動かしたいという件ってありませんか?
私は普通にあります。ここでは、最新の環境(debian unstable)にて、python3.6を動かそうとしておおはまりした件をお話しようとおもいます。
python3.6がSegmentaion fault
pythonのいろいろなバージョンが欲しいときいろいろなpythonの仮想環境を作る方も多いと思います。自分は主にpyenvを使っています。
今回どうしてもpython3.6の環境を用意する必要があり、以下のように最新のOSであるところのdebian unstableへインストールを試みました1。
$ lsb-release -a
Distributor ID: Debian
Description: Debian GNU/Linux bookworm/sid
Release: unstable
Codename: sid
$ gcc -v
...中略...
Supported LTO compression algorithms: zlib zstd
gcc version 11.2.0 (Debian 11.2.0-16)
$ pyenv install -k 3.6.15
Downloading Python-3.6.15.tar.xz...
-> https://www.python.org/ftp/python/3.6.15/Python-3.6.15.tar.xz
Installing Python-3.6.15...
BUILD FAILED (Debian unstable using python-build 2.1.0-12-g5963dc4f)
Inspect or clean up the working tree at /home/yours/.pyenv/sources/3.6.15
Results logged to /tmp/python-build.20220222021843.38052.log
Last 10 log lines:
if test "xupgrade" != "xno" ; then \
case upgrade in \
upgrade) ensurepip="--upgrade" ;; \
install|*) ensurepip="" ;; \
esac; \
./python -E -m ensurepip \
$ensurepip --root=/ ; \
fi
Segmentation fault
make: *** [Makefile:1102: install] エラー 139
アイエエエー!?Segmentaion faultナンデ!?Segmentaion faultナンデ!?2
見事に導入失敗です。これは困りました。似たようなissueが無いか?をgoogleってみましたが、検索してもレポートも対処方法も見つかりません。みんなpypiでpython3.6をサポートしているという掲示も多いのにpython3.6のtestはどうしてるんだろう…?
Segmentation faultが起きるハズは…ない
賢明なpython使いの皆様は、ここで早速faulthandler活用されると思います。
$ cd ~/.pyenv/sources/3.6.15/Python-3.6.15/
$ ./python -X faulthandler -E -m ensurepip --root=/
Fatal Python error: Segmentation fault
Current thread 0x00007f20bdfc3500 (most recent call first):
File "/home/yours/.pyenv/sources/3.6.15/Python-3.6.15/Lib/ctypes/__init__.py", line 273 in _reset_cache
File "/home/yours/.pyenv/sources/3.6.15/Python-3.6.15/Lib/ctypes/__init__.py", line 538 in <module>
...中略...
ctypes/__init__.pyの273行目でSegmentation faultです。
$ sed -ne '273p' /home/yours/.pyenv/sources/3.6.15/Python-3.6.15/Lib/ctypes/__init__.py
CFUNCTYPE(c_int)(lambda: None)
なんと、ctypesによる関数呼び出しを定義しようとしてSegmentation faultです。実はこの記述よく見ると正直意味が無いコードではありますが3、それでもSegmentation faultになって良いような内容ではありません。
途中のアセンブラでSegmentation fault
python3.6の深界へバグを追って行かねばなりません。早速gdb使って降りていきます…。
$ cd ~/.pyenv/sources/3.6.15/Python-3.6.15/
$ gdb --args ./python -X faulthandler -E -m ensurepip --root=/
GNU gdb (Debian 10.1-2) 10.1.90.20210103-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
...中略...
(gdb) run
Starting program: /home/yours/.pyenv/sources/3.6.15/Python-3.6.15/python -X faulthandler -E -m ensurepip --root=/
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff6bfe5de in PyCFuncPtr_new (type=0x555555c201f8, args=0x7ffff6c1b860, kwds=0x0)
at /home/yours/.pyenv/sources/3.6.15/Python-3.6.15/Modules/_ctypes/_ctypes.c:3557
3557 self->thunk = thunk;
(gdb) p self
$1 = (PyCFuncPtrObject *) 0x7ffff6a5fb38
(gdb) p thunk
$2 = (CThunkObject *) 0x7ffff65620e0
(gdb) p &(self->thunk)
$3 = (CThunkObject **) 0x7ffff6a5fb98
python言語の層から、C言語の層に降りてみましたが、self変数のメモリ確保については、なんとなく問題なさそうです。さらに次の層である、x86/64アセンブラの層に降りないといけなさそうです。自分も普段ここまでくることは滅多にないので、面倒くささのあまり「これ以上降りると上昇負荷で帰ってこれない…」とやらないで済む言い訳を思いつきましたがやるしかありません。
(gdb) b 3557
Breakpoint 1 at 0x7ffff6bfe5d6: file /home/yours/.pyenv/sources/3.6.15/Python-3.6.15/Modules/_ctypes/_ctypes.c, line 3557.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/yours/.pyenv/sources/3.6.15/Python-3.6.15/python -X faulthandler -E -m ensurepip --root=/
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, PyCFuncPtr_new (type=0x555555c201f8, args=0x7ffff6bda860, kwds=0x0)
at /home/yours/.pyenv/sources/3.6.15/Python-3.6.15/Modules/_ctypes/_ctypes.c:3557
3557 self->thunk = thunk;
(gdb) disas
...中略...
0x00007ffff6bbd5bd <+221>: je 0x7ffff6bbd900 <PyCFuncPtr_new+1056>
0x00007ffff6bbd5c3 <+227>: mov 0x78(%rsp),%rax
0x00007ffff6bbd5c8 <+232>: movq %r15,%xmm0
0x00007ffff6bbd5cd <+237>: movq %rax,%xmm1
0x00007ffff6bbd5d2 <+242>: addq $0x1,(%rax)
=> 0x00007ffff6bbd5d6 <+246>: mov 0x10(%r13),%rax
0x00007ffff6bbd5da <+250>: punpcklqdq %xmm1,%xmm0
0x00007ffff6bbd5de <+254>: movaps %xmm0,0x60(%r13)
0x00007ffff6bbd5e3 <+259>: mov 0x20(%r15),%rdx
...中略...
バグを追い詰めていきます。x86/64のアセンブラの層はlayout asmというgdbの機能で表示を工夫すると見やすいですね。
(gdb) layout asm
│ 0x7ffff6bbd5c8 <PyCFuncPtr_new+232> movq %r15,%xmm0 │
│ 0x7ffff6bbd5cd <PyCFuncPtr_new+237> movq %rax,%xmm1 │
│ 0x7ffff6bbd5d2 <PyCFuncPtr_new+242> addq $0x1,(%rax) │
│B+ 0x7ffff6bbd5d6 <PyCFuncPtr_new+246> mov 0x10(%r13),%rax │
│ 0x7ffff6bbd5da <PyCFuncPtr_new+250> punpcklqdq %xmm1,%xmm0 │
│ >0x7ffff6bbd5de <PyCFuncPtr_new+254> movaps %xmm0,0x60(%r13) │
│ 0x7ffff6bbd5e3 <PyCFuncPtr_new+259> mov 0x20(%r15),%rdx │
│ 0x7ffff6bbd5e7 <PyCFuncPtr_new+263> mov %rdx,(%rax) │
│ 0x7ffff6bbd5ea <PyCFuncPtr_new+266> addq $0x1,(%r15) │
multi-thre Thread 0x7ffff7c885 In: PyCFuncPtr_new L3557 PC: 0x7ffff6bbd5de
Starting program: /home/yours/.pyenv/sources/3.6.15/Python-3.6.15/python -X faulthandler -E -m ensurepip --root=/
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, PyCFuncPtr_new (type=0x555555c201f8, args=0x7ffff6bda860, kwds=0x0)
at /home/yours/.pyenv/sources/3.6.15/Python-3.6.15/Modules/_ctypes/_ctypes.c:3557
(gdb) si
(gdb) si
(gdb) si
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff6bbd5de in PyCFuncPtr_new (type=0x555555c201f8, args=0x7ffff6bda860, kwds=0x0)
at /home/yours/.pyenv/sources/3.6.15/Python-3.6.15/Modules/_ctypes/_ctypes.c:3557
見つけました!こいつです!
>0x7ffff6bbd5de <PyCFuncPtr_new+254> movaps %xmm0,0x60(%r13) │
ついでにr13レジスタも確認しておきます。
(gdb) p/x $r13
$1 = 0x7ffff6d21b38
(gdb) p/x $xmm0
$2 = {v8_bfloat16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0},
v16_int8 = {0xe0, 0x30, 0x56, 0xf6, 0xff, 0x7f, 0x0, 0x0, 0x10, 0x15, 0xbe, 0xf6, 0xff, 0x7f, 0x0, 0x0}, v8_int16 = {
0x30e0, 0xf656, 0x7fff, 0x0, 0x1510, 0xf6be, 0x7fff, 0x0}, v4_int32 = {0xf65630e0, 0x7fff, 0xf6be1510, 0x7fff},
v2_int64 = {0x7ffff65630e0, 0x7ffff6be1510}, uint128 = 0x7ffff6be151000007ffff65630e0}
アドレスが16bytes境界じゃないぞ!?
movaps命令とはSSE命令と呼ばれるx86系の命令で、4バイト(32bit)4つ分のデータをxmm0レジスタ(128bit)へ一撃で転送してくれるという超カッコイイ命令です。ところが、データの開始が16bytes境界に置かれていないとSegmentation faultしてしまうという制約があります。では、r13のレジスタの値は?というと、かならず末尾が0でないと16bytes境界ではありませんから、8(0x7ffff6d21b38,!=0)であることで「こりゃおかしい!」となります。
これでは僕のPCのIntel Xeon様もお怒りです。gcc-11は-O3の元では、一定の条件が揃うと、デフォルトでmovaps命令を使おうとするアセンブラを生成してしまうので、PyObjectとしてPythonのGCから刈り取ってくるメモリはいつでも16bytes 境界になってないといけません。
python3.7以上向けのパッチで治った
python本家のbug reportで似たような報告がないか?と、“PyObject 16bytes alignment bug"をキーワードにgoogle検索すると真っ先に
https://bugs.python.org/issue27987
が見つかりました。どうもpython 3.7では16 bytes境界に治した人がいるようですが、3.6は未適用のようです。
仕方がないので、3.6に自力でパッチを当ててみます。issueを読むと、
-
https://github.com/python/cpython/commit/8766cb74e186d3820db0a855ccd780d6d84461f7
-
https://github.com/python/cpython/commit/f0be4bbb9b3cee876249c23f2ae6f38f43fa7495
の2つを取りこめばよいらしいので、早速当ててinstallしてみます。
$ (for url in https://github.com/python/cpython/commit/8766cb74e186d3820db0a855ccd780d6d84461f7.patch \
https://github.com/python/cpython/commit/f0be4bbb9b3cee876249c23f2ae6f38f43fa7495.patch;
do curl $url;echo;echo; done ) | pyenv -p -k install 3.6.15
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1859 100 1859 0 0 7381 0 --:--:-- --:--:-- --:--:-- 7406
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1687 100 1687 0 0 6656 0 --:--:-- --:--:-- --:--:-- 6667
Installing Python-3.6.15...
patching file Include/objimpl.h
patching file 'Misc/NEWS.d/next/Core and Builtins/2019-05-15-18-28-43.bpo-27987.FaxuLy.rst'
patching file 'Misc/NEWS.d/next/Core and Builtins/2019-04-16-11-52-21.bpo-27987.n2_DcQ.rst'
patching file Objects/obmalloc.c
Hunk #1 succeeded at 650 (offset -145 lines).
Installed Python-3.6.15 to /home/yours/.pyenv/versions/3.6.15
$ python3.6 -m pip
...無事に動く...
やったー!治りましたーっ。これでdebian unstable & gcc-11のような最新環境でも無事python3.6が動きました!
python本家に報告したら…残念!
今回の記事では、python3.7用の対策パッチを当てることで治しましたが、他にも、
- gcc-10を使うとか、
$ env CC=gcc-10 pyenv install 3.6.15
- 最適化を-O2に格下げしてSSE命令が使われないようにするとか
$ env CFLAGS=-O2 pyenv install 3.6.15
もあります。これらを添えて、bugs.python.orgのissueに報告してみました。
今回紹介のパッチを当ててpython3.6をアップデートしてくれない?とレポートしましたが残念ながら、EOLが近いということで対応無しでcloseとなってしまいました。まあ、同じことではまる人向けにworkaroundもissueに記載したので、よし!としています。めでたしめでたし!
-
python3.6は2021年12月でEOLとなっています。でも古いバージョンのバイナリ欲しい!という用途は依然としてありますよね…。 ↩︎
-
実際には、toxというpython用のtestフレームワークでこの問題にあたったのが発端ですが、このあたりを書くと長いので いきなりpyenv install -k 3.6.15中にSegmentation faultに遭遇したということから始めます。なお、普通にpyenv install 3.6.15 (-kオプション無)でインストールをすると何の問題もなくpython3.6の導入が完了してしまいますが、そのあとpython3.6 -m pipすると全く同じ問題が発生します。 ↩︎
-
ctypes/__init__.pyの269-272行目にこのコードが必要な理由がコメントとして記載されています。どうもWin64環境ではこのコードが無いと動かないようです。 ↩︎