スクエニ ITエンジニア ブログ

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を読むと、

の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に記載したので、よし!としています。めでたしめでたし!


  1. python3.6は2021年12月でEOLとなっています。でも古いバージョンのバイナリ欲しい!という用途は依然としてありますよね…。 ↩︎

  2. 実際には、toxというpython用のtestフレームワークでこの問題にあたったのが発端ですが、このあたりを書くと長いので いきなりpyenv install -k 3.6.15中にSegmentation faultに遭遇したということから始めます。なお、普通にpyenv install 3.6.15 (-kオプション無)でインストールをすると何の問題もなくpython3.6の導入が完了してしまいますが、そのあとpython3.6 -m pipすると全く同じ問題が発生します。 ↩︎

  3. ctypes/__init__.pyの269-272行目にこのコードが必要な理由がコメントとして記載されています。どうもWin64環境ではこのコードが無いと動かないようです。 ↩︎

この記事を書いた人

記事一覧
SQUARE ENIXでは一緒に働く仲間を募集しています!
興味をお持ちいただけたら、ぜひ採用情報ページもご覧下さい!