gcc-10.0.1固有のSegfault


23

私はかなり長い間比較的安定していて、さまざまなプラットフォームとコンパイラ(windows / osx / debian / fedora gcc / clang)に対して頻繁にテストされているCコンパイル済みコードを含むRパッケージを持っています

最近、パッケージを再度テストするために新しいプラットフォームが追加されました:

Logs from checks with gcc trunk aka 10.0.1 compiled from source
on Fedora 30. (For some archived packages, 10.0.0.)

x86_64 Fedora 30 Linux

FFLAGS="-g -O2 -mtune=native -Wall -fallow-argument-mismatch"
CFLAGS="-g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection"
CXXFLAGS="-g -O2 -Wall -pedantic -mtune=native -Wno-ignored-attributes -Wno-deprecated-declarations -Wno-parentheses -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection"

その時点で、コンパイルされたコードはすぐに次の行に沿ってsegfaultingを開始しました。

 *** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'

最適化レベルでDocker rocker/r-baseコンテナーを使用することにより、segfaultを一貫して再現できました。低い最適化を実行すると、問題が解消されます。valgrind(-O0と-O2の両方)、UBSAN(gcc / clang)を含む他のセットアップを実行しても、まったく問題はありません。また、これはで実行されたと合理的に確信していますが、データがありません。gcc-10.0.1-O2gcc-10.0.0

私はgcc-10.0.1 -O2バージョンを実行しましたが、gdb私には奇妙に見えることに気づきました:

gdbとコード

強調表示されたセクションをステップ実行している間、配列の2番目の要素の初期化がスキップされているように見えます(Rに制御を返すときに自己ガベージコレクションのR_allocラッパーmallocです。Rに戻る前にsegfaultが発生します)。その後、初期化されていない要素(gcc.10.0.1 -O2バージョン)にアクセスすると、プログラムがクラッシュします。

問題の要素を最終的に要素の使用につながるコードのすべての場所で明示的に初期化することによってこれを修正しましたが、実際には空の文字列に初期化されている必要があり、少なくとも私が想定したとおりです。

私は明白な何かを逃したり、愚かなことをしていますか?Cはで私の第二言語であるとして、どちらが合理的に可能性がありますはるか。これが今ちょうど現れたのは奇妙で、コンパイラが何をしようとしているのか理解できません。


更新:これを再現する手順。ただし、debian:testingDockerコンテナがにある場合に限り再現さgcc-10gcc-10.0.1ます。また、私を信頼していない場合は、これらのコマンドを実行しないでください

申し訳ありませんが、これは最小限の再現可能な例ではありません。

docker pull rocker/r-base
docker run --rm -ti --security-opt seccomp=unconfined \
  rocker/r-base /bin/bash
apt-get update
apt-get install gcc-10 gdb
gcc-10 --version  # confirm 10.0.1
# gcc-10 (Debian 10-20200222-1) 10.0.1 20200222 (experimental) 
# [master revision 01af7e0a0c2:487fe13f218:e99b18cf7101f205bfdd9f0f29ed51caaec52779]

mkdir ~/.R
touch ~/.R/Makevars
echo "CC = gcc-10
CFLAGS = -g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection
" >> ~/.R/Makevars

R -d gdb --vanilla

次に、Rコンソールで、プログラムを実行runするためgdbに入力した後:

f.dl <- tempfile()
f.uz <- tempfile()

github.url <- 'https://github.com/brodieG/vetr/archive/v0.2.8.zip'

download.file(github.url, f.dl)
unzip(f.dl, exdir=f.uz)
install.packages(
  file.path(f.uz, 'vetr-0.2.8'), repos=NULL,
  INSTALL_opts="--install-tests", type='source'
)
# minimal set of commands to segfault
library(vetr)
alike(pairlist(a=1, b="character"), pairlist(a=1, b=letters))
alike(pairlist(1, "character"), pairlist(1, letters))
alike(NULL, 1:3)                  # not a wild card at top level
alike(list(NULL), list(1:3))      # but yes when nested
alike(list(NULL, NULL), list(list(list(1, 2, 3)), 1:25))
alike(list(NULL), list(1, 2))
alike(list(), list(1, 2))
alike(matrix(integer(), ncol=7), matrix(1:21, nrow=3))
alike(matrix(character(), nrow=3), matrix(1:21, nrow=3))
alike(
  matrix(integer(), ncol=3, dimnames=list(NULL, c("R", "G", "B"))),
  matrix(1:21, ncol=3, dimnames=list(NULL, c("R", "G", "B")))
)

# Adding tests from docs

mx.tpl <- matrix(
  integer(), ncol=3, dimnames=list(row.id=NULL, c("R", "G", "B"))
)
mx.cur <- matrix(
  sample(0:255, 12), ncol=3, dimnames=list(row.id=1:4, rgb=c("R", "G", "B"))
)
mx.cur2 <-
  matrix(sample(0:255, 12), ncol=3, dimnames=list(1:4, c("R", "G", "B")))

alike(mx.tpl, mx.cur2)

CSR_strmlen_x初期化されていない文字列にアクセスしようとしていることを(私が正しく理解していれば)gdbで調べると、かなりすばやく表示さ れます。

UPDATE 2:これは非常に再帰的な関数であり、その上、文字列初期化ビットが何度も呼び出されます。これはほとんどb / cです。怠惰でした。再帰で報告したいものに実際に遭遇したときに一度だけ初期化された文字列が必要ですが、何かに遭遇する可能性があるたびに初期化する方が簡単でした。次に説明するのは複数の初期化を示していますが、そのうちの1つ(おそらくアドレス<0x1400000001>を持つもの)のみが使用されているためです。

私がここに表示しているものがsegfaultを引き起こした要素に直接関連していることは保証できません(それは同じ不正なアドレスアクセスですが)、@ nate-eldredgeが尋ねたように、配列要素が呼び出し関数での復帰直前または復帰直後のいずれかに初期化されます。呼び出し側の関数がこれらのうち8つを初期化していることに注意してください。すべてがゴミまたはアクセスできないメモリで満たされているので、すべて表示しています。

ここに画像の説明を入力してください

UPDATE 3、問題の機能の分解:

Breakpoint 1, ALIKEC_res_strings_init () at alike.c:75
75    return res;
(gdb) p res.current[0]
$1 = 0x7ffff46a0aa5 "%s%s%s%s"
(gdb) p res.current[1]
$2 = 0x1400000001 <error: Cannot access memory at address 0x1400000001>
(gdb) disas /m ALIKEC_res_strings_init
Dump of assembler code for function ALIKEC_res_strings_init:
53  struct ALIKEC_res_strings ALIKEC_res_strings_init() {
   0x00007ffff4687fc0 <+0>: endbr64 

54    struct ALIKEC_res_strings res;

55  
56    res.target = (const char **) R_alloc(5, sizeof(const char *));
   0x00007ffff4687fc4 <+4>: push   %r12
   0x00007ffff4687fc6 <+6>: mov    $0x8,%esi
   0x00007ffff4687fcb <+11>:    mov    %rdi,%r12
   0x00007ffff4687fce <+14>:    push   %rbx
   0x00007ffff4687fcf <+15>:    mov    $0x5,%edi
   0x00007ffff4687fd4 <+20>:    sub    $0x8,%rsp
   0x00007ffff4687fd8 <+24>:    callq  0x7ffff4687180 <R_alloc@plt>
   0x00007ffff4687fdd <+29>:    mov    $0x8,%esi
   0x00007ffff4687fe2 <+34>:    mov    $0x5,%edi
   0x00007ffff4687fe7 <+39>:    mov    %rax,%rbx

57    res.current = (const char **) R_alloc(5, sizeof(const char *));
   0x00007ffff4687fea <+42>:    callq  0x7ffff4687180 <R_alloc@plt>

58  
59    res.target[0] = "%s%s%s%s";
   0x00007ffff4687fef <+47>:    lea    0x1764a(%rip),%rdx        # 0x7ffff469f640
   0x00007ffff4687ff6 <+54>:    lea    0x18aa8(%rip),%rcx        # 0x7ffff46a0aa5
   0x00007ffff4687ffd <+61>:    mov    %rcx,(%rbx)

60    res.target[1] = "";

61    res.target[2] = "";
   0x00007ffff4688000 <+64>:    mov    %rdx,0x10(%rbx)

62    res.target[3] = "";
   0x00007ffff4688004 <+68>:    mov    %rdx,0x18(%rbx)

63    res.target[4] = "";
   0x00007ffff4688008 <+72>:    mov    %rdx,0x20(%rbx)

64  
65    res.tar_pre = "be";

66  
67    res.current[0] = "%s%s%s%s";
   0x00007ffff468800c <+76>:    mov    %rax,0x8(%r12)
   0x00007ffff4688011 <+81>:    mov    %rcx,(%rax)

68    res.current[1] = "";

69    res.current[2] = "";
   0x00007ffff4688014 <+84>:    mov    %rdx,0x10(%rax)

70    res.current[3] = "";
   0x00007ffff4688018 <+88>:    mov    %rdx,0x18(%rax)

71    res.current[4] = "";
   0x00007ffff468801c <+92>:    mov    %rdx,0x20(%rax)

72  
73    res.cur_pre = "is";

74  
75    return res;
=> 0x00007ffff4688020 <+96>:    lea    0x14fe0(%rip),%rax        # 0x7ffff469d007
   0x00007ffff4688027 <+103>:   mov    %rax,0x10(%r12)
   0x00007ffff468802c <+108>:   lea    0x14fcd(%rip),%rax        # 0x7ffff469d000
   0x00007ffff4688033 <+115>:   mov    %rbx,(%r12)
   0x00007ffff4688037 <+119>:   mov    %rax,0x18(%r12)
   0x00007ffff468803c <+124>:   add    $0x8,%rsp
   0x00007ffff4688040 <+128>:   pop    %rbx
   0x00007ffff4688041 <+129>:   mov    %r12,%rax
   0x00007ffff4688044 <+132>:   pop    %r12
   0x00007ffff4688046 <+134>:   retq   
   0x00007ffff4688047:  nopw   0x0(%rax,%rax,1)

End of assembler dump.

更新4

したがって、ここで標準を解析しようとすると、関連があるように見える部分があります(C11ドラフト):

6.3.2.3 Par7変換>その他のオペランド>ポインタ

オブジェクト型へのポインタは、別のオブジェクト型へのポインタに変換される場合があります。 結果のポインターが参照された型に対して正しく配置されていない場合 68)、動作は未定義です。
それ以外の場合、再度変換すると、結果は元のポインタと同じになります。オブジェクトへのポインターが文字型へのポインターに変換されると、結果はオブジェクトのアドレス指定された最下位バイトを指します。オブジェクトのサイズまで、結果を連続してインクリメントすると、オブジェクトの残りのバイトへのポインターが生成されます。

6.5 Par6式

格納された値にアクセスするためのオブジェクトの有効なタイプは、オブジェクトの宣言されたタイプです(存在する場合)。87)文字型ではない型を持つ左辺値を通じて宣言された型を持たないオブジェクトに値が格納されている場合、左辺値の型は、そのアクセスおよびそれ以降のアクセスではないオブジェクトの有効な型になります保存された値を変更します。値がmemcpyまたはmemmoveを使用して宣言されたタイプを持たないオブジェクトにコピーされるか、文字タイプの配列としてコピーされる場合、そのアクセスおよび値を変更しない後続のアクセスの変更されたオブジェクトの有効なタイプは、値のコピー元のオブジェクトの有効なタイプ(存在する場合)。 宣言された型を持たないオブジェクトへの他のすべてのアクセスの場合、オブジェクトの有効な型は、アクセスに使用される左辺値の型になります。

87)割り当てられたオブジェクトには宣言された型がありません。

IIUC R_allocは、整列mallocが保証されているedブロックへdoubleのオフセットと、オフセット後のブロックのサイズが要求されたサイズになります(R固有のデータのオフセットの前にも割り当てがあります)。 戻り時にR_allocそのポインタをキャストします(char *)

セクション6.2.5パー29

voidへのポインタは、文字型へのポインタと同じ表現および配置要件を持たなければならない。48)同様に、互換性のある型の修飾されたバージョンまたは修飾されていないバージョンへのポインタは、同じ表現と配置の要件を持たなければならない。構造体型へのすべてのポインタは、互いに同じ表現と配置の要件を持たなければなりません。
共用体型へのすべてのポインタは、互いに同じ表現と配置の要件を持たなければなりません。
他の型へのポインターは、同じ表現または配置要件を持つ必要はありません。

48)同じ表現と配置の要件は、関数、関数からの戻り値、および共用体のメンバーへの互換性のある引数を意味します。

したがって、問題は「toを再キャストし(char *)(const char **)それに書き込むことを許可されているか(const char **)」です。上記の私の読みは、コードが実行されるシステム上のポインターが配置と互換性のあるdouble配置である限り、それで問題ありません。

「厳密なエイリアス」に違反していますか?つまり:

6.5パー7

オブジェクトは、次のいずれかのタイプの左辺値式によってのみアクセスされる格納された値を持つものとします。88)

—オブジェクトの有効なタイプと互換性のあるタイプ...

88)このリストの目的は、オブジェクトがエイリアスされる場合とされない場合がある状況を指定することです。

では、コンパイラは(または)が指すオブジェクトの有効なタイプをどのように考えるべきでしょうか?おそらく宣言された型、またはこれは実際にあいまいですか?同じオブジェクトにアクセスするスコープ内に他の「左辺値」がないため、この場合だけではないように感じます。res.targetres.current(const char **)

私は、標準のこれらのセクションから意味を引き出すために力を尽くして努力していることを認めます。


まだ調べていない場合は、分解を調べて、何が行われているかを正確に確認することをお勧めします。また、gccバージョン間の逆アセンブリを比較します。
カイルム

2
GCCのトランクバージョンをいじろうとはしません。楽しむのはいいことですが、理由からトランクと呼ばれています。残念ながら、(1)コードと正確な構成(2)同じアーキテクチャーの同じGCCバージョン(3)がないと、何が問題なのかを判断することはほとんど不可能です。10.0.1がトランクから安定版に移行するときに、これが続くかどうかを確認することをお勧めします。
Marco Bonelli

1
もう1つのコメント:-mtune=nativeマシンに搭載されている特定のCPU向けに最適化します。これはテスターに​​よって異なり、問題の一部である可能性があります。コンパイルを実行すると-v、マシン(たとえば-mtune=skylake、私のコンピューター)にあるCPUファミリーを確認できます。
Nate Eldredge

1
デバッグ実行からはまだわかりません。分解は決定的でなければなりません。何も抽出する必要はありません。プロジェクトをコンパイルして逆アセンブルしたときに生成された.oファイルを見つけるだけです。disassemblegdb内の命令を使用することもできます。
Nate Eldredge

5
とにかく、おめでとうございます、あなたは問題が実際にコンパイラのバグであったまれな数人の一人です。
Nate Eldredge

回答:


22

概要:これは文字列の最適化に関連するgccのバグのようです。自己完結型のテストケースを以下に示します。最初はコードが正しいかどうか疑問がありましたが、私は正しいと思います。

バグをPR 93982として報告しました提案された修正はコミットされましたが、すべての場合に修正されるわけではなく、フォローアップPR 94015godbolt link)に至りました

フラグを付けてコンパイルすることで、バグを回避できるはず-fno-optimize-strlenです。


テストケースを次の最小限の例に減らすことができました(godboltでも同様)。

struct a {
    const char ** target;
};

char* R_alloc(void);

struct a foo(void) {
    struct a res;
    res.target = (const char **) R_alloc();
    res.target[0] = "12345678";
    res.target[1] = "";
    res.target[2] = "";
    res.target[3] = "";
    res.target[4] = "";
    return res;
}

gccトランク(gccバージョン10.0.1 20200225(試験的))および-O2(他のすべてのオプションは不要であることが判明した)、amd64で生成されたアセンブリは次のようになります。

.LC0:
        .string "12345678"
.LC1:
        .string ""
foo:
        subq    $8, %rsp
        call    R_alloc
        movq    $.LC0, (%rax)
        movq    $.LC1, 16(%rax)
        movq    $.LC1, 24(%rax)
        movq    $.LC1, 32(%rax)
        addq    $8, %rsp
        ret

したがって、コンパイラーが初期化に失敗していることはご存じのとおりですres.target[1](が目立っていないことに注意してくださいmovq $.LC1, 8(%rax))。

コードをいじって、「バグ」に何が影響するかを見るのは興味深いことです。おそらく大幅に、の戻り値の型をR_allocに変更すると、戻り値void *がなくなり、「正しい」アセンブリ出力が得られます。あまり重要ではないかもしれませんが、もっとおもしろいかもしれませんが、文字列"12345678"を長くしたり短くしたりすると、文字列が消えてしまいます。


以前の議論、現在は解決-コードは明らかに合法です。

私の質問は、あなたのコードが実際に合法であるかどうかです。あなたが取るという事実char *によって返されたR_alloc()し、それをキャストconst char **した後、店はconst char *それが違反する可能性があるように思える厳格なエイリアシング規則を通り、charおよびconst char *互換性のある型ではありません。char(のようなものを実装するためにmemcpy)任意のオブジェクトにアクセスできるようにする例外がありますが、これは逆の方法であり、私が理解している限りでは許可されていません。これにより、コードで未定義の動作が生成されるため、コンパイラーは必要なことを何でも合法的に実行できます。

これがそうである場合、正しい修正は、R R_alloc()がのvoid *代わりに戻るようにコードを変更することですchar *。その後、エイリアシングの問題はありません。残念ながら、そのコードはあなたの管理外にあり、厳密なエイリアシングに違反せずにこの関数をどのように使用できるかははっきりしていません。回避策は、たとえばvoid *tmp = R_alloc(); res.target = tmp;テストケースの問題を解決する一時変数を挿入することですが、それが正当かどうかはまだわかりません。

ただし、この「厳密なエイリアシング」の仮説については確信がありません-fno-strict-aliasing。AFAIKがでコンパイルすると、gccがそのような構成を許可することになっているため、問題が解決するわけではありませ


更新。いくつかの異なるオプションを試したところ、「正しい」コードが生成されるか、-fno-optimize-strlenまたは-fno-tree-forwprop生成されることがわかりました。また、使用-O1 -foptimize-strlenすると誤ったコードが生成されます(ただし、生成され-O1 -ftree-forwpropません)。

少しgit bisect練習したところ、エラーはcommit 34fcf41e30ff56155e996f5e04で発生したようです


アップデート2.学べることを確認するためだけに、gccソースを少し掘り下げてみました。(私は、コンパイラの専門家であるとは主張していません!)

のコードtree-ssa-strlen.cは、プログラムに表示される文字列を追跡することを目的としているようです。私が知る限りでは、バグはステートメントを見るとres.target[0] = "12345678";、コンパイラが文字列リテラルのアドレス"12345678"文字列自体で圧縮することです。(これは、前述のコミットで追加されたこの疑わしいコードに関連しいるようです。実際にアドレスである「文字列」のバイトをカウントしようとすると、代わりにそのアドレスが指すものを調べます。)

文はと思って、それはそうres.target[0] = "12345678"ではなく、保存のアドレス"12345678"アドレスでのres.target声明であるかのように、そのアドレスに文字列そのものを記憶していますstrcpy(res.target, "12345678")。これにより、末尾のnulがアドレスに格納されることに注意してくださいres.target+8(コンパイラのこの段階では、すべてのオフセットはバイト単位です)。

コンパイラーがを見るとres.target[1] = ""、同様にこれをあたかも扱っているかのように処理しますstrcpy(res.target+8, "")。8はのサイズに由来しますchar *。つまり、単にnulバイトをaddressに格納しているかのようres.target+8です。ただし、コンパイラは、前のステートメントがそのアドレスにすでにヌルバイトを格納していることを「認識」しています。そのため、このステートメントは「冗長」であり、破棄できます(here)。

これは、バグをトリガーするために文字列が正確に8文字でなければならない理由を説明しています。(他の8の倍数も、他の状況でバグをトリガーする可能性があります。)


異なるタイプのポインタへのFWIWの再キャストが文書化されています。に再キャストしてもよいint*が、そうではないかどうかを知るためのエイリアシングについては知りませんconst char**
BrodieG

厳密なエイリアシングについての私の理解が正しい場合、キャスト先int *も違法です(または、実際にintそこにsを格納することは違法です)。
Nate Eldredge

1
これは厳密なエイリアシングルールとは関係ありません。厳密なエイリアシングルールは、別のハンドルを使用して既に保存したデータにアクセスすることです。ここで割り当てるだけなので、厳密なエイリアスルールには影響しません。ポインターのキャストは、両方のポインタータイプのアライメント要件が同じ場合に有効ですが、ここではx86_64 からキャストして作業しています...ここにはUBがありません。これはgccのバグです。char*
KamilCuk

1
はい、いいえ、@ KamilCuk。標準の用語では、「アクセス」には、オブジェクトの値の読み取りと変更の両方が含まれます。したがって、厳密なエイリアシングルールは「保存」を意味します。リードバック操作に限定されません。しかし、型が宣言されていないオブジェクトの場合、そのようなオブジェクトへの書き込みにより、その有効な型が書き込まれた内容に対応するように自動的に変更されるという事実によって、それは動機付けられます。宣言された型のないオブジェクトは、動的に割り当てられたオブジェクトであり(アクセスされるポインターの型に関係なく)、実際にSA違反はありません。
ジョンボリンジャー

2
はい、@ Nateはその定義でR_alloc()、どの翻訳単位R_alloc()が定義されているかに関係なく、プログラムは適合しています。ここで適合しないのはコンパイラです。
ジョンボリンジャー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.