gcc-10.0.1特定段错误


23

我有一个带有C编译代码的R软件包,该软件包相当稳定了一段时间,并且经常针对各种平台和编译器(windows / osx / debian / fedora gcc / clang)进行测试。

最近,添加了一个新平台来再次测试该软件包:

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"

在这一点上,已编译的代码立即按照以下方式开始进行段错误处理:

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

通过使用具有优化级别的rocker/r-basedocker容器,我能够始终如一地再现段错误。运行较低的优化可以解决该问题。运行任何其他设置,包括在valgrind(在-O0和-O2下),UBSAN(gcc / clang)下,都没有任何问题。我也有理由确定这是根据,但没有数据。gcc-10.0.1-O2gcc-10.0.0

我使用来运行该gcc-10.0.1 -O2版本,gdb并发现对我来说似乎有些奇怪:

gdb vs代码

逐步执行突出显示的部分时,似乎跳过了数组第二个元素的初始化(R_allocmalloc将控制权返回给R时自垃圾回收的包装;分段故障发生在返回R之前)。后来,当访问未初始化的元素(在gcc.10.0.1 -O2版本中)时,程序崩溃。

我通过在代码的各处显式初始化有问题的元素(最终导致该元素的使用)来解决此问题,但实际上应该已将其初始化为空字符串,或者至少这就是我所假设的。

我是否缺少明显的东西或做愚蠢的事情?到目前为止,由于C是我的第二语言,所以两者都有可能。奇怪的是,现在这个问题突然出现了,我不知道编译器在试图做什么。


更新:重现此指令,尽管仅在debian:testingDocker容器gcc-10位于的情况下才会重现gcc-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)

在gdb中进行检查很快就会显示(如果我理解正确的话) CSR_strmlen_x正在尝试访问未初始化的字符串。

UPDATE 2:这是一个高度递归的函数,最重要的是,字符串初始化位被调用了很多次。这主要是b / c,我很懒,我们只需要在一次递归中实际遇到要报告的内容时初始化字符串,但是每次遇到可能要初始化时都更容易初始化。我之所以这样说,是因为您接下来将看到的显示了多个初始化,但是仅使用其中一个(可能是地址为<0x1400000001>的初始化)。

我不能保证我在这里显示的内容与导致段错误的元素直接相关(尽管这是相同的非法地址访问),但是正如@ n​​ate-eldredge要求的那样,它确实表明数组元素不是在调用函数中在返回之前或返回之后立即初始化。请注意,调用函数正在初始化其中的8个,我将全部显示给它们,它们全部填满了垃圾或无法访问的内存。

在此处输入图片说明

更新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保证double对齐的ed块中,并且偏移量之后的块的大小为请求的大小(R特定数据的偏移量之前也有分配)。 在返回时R_alloc强制转换该指针(char *)

6.2.5节29项

指向void的指针应具有与字符类型的指针相同的表示和对齐要求。48)同样,指向兼容类型的合格或不合格版本的指针应具有相同的表示和对齐要求。所有指向结构类型的指针应具有相同的表示和对齐要求。
指向联合类型的所有指针应具有相同的表示和对齐要求。
指向其他类型的指针不必具有相同的表示或对齐要求。

48)相同的表示形式和对齐要求旨在暗含对函数的互换性参数,函数的返回值以及并集成员。

所以,问题是“我们现在可以重铸(char *)(const char **)和写入的(const char **)。” 我对以上内容的理解是,只要运行代码的系统上的指针具有与double对齐方式兼容的对齐方式,那么就可以了。

我们是否违反“严格混叠”?即:

6.5标准杆7

一个对象只能通过具有以下类型之一的左值表达式访问其存储值:88)

—与对象的有效类型兼容的类型...

88)此列表的目的是指定对象可能会别名或可能不会别名的那些情况。

那么,编译器应该怎么看待(或)指向的对象的有效类型呢?大概是声明的类型,或者这实际上是模棱两可的吗?在我看来,这不仅是因为范围内没有其他访问同一对象的“左值”。res.targetres.current(const char **)

我承认我正在竭尽全力从标准的这些部分中提取意义。


如果尚未进行检查,则值得查看一下拆卸程序,以确切了解正在执行的操作。并比较gcc版本之间的反汇编。
kaylum

2
我不会尝试弄乱GCC的主干版本。很高兴玩,但是有一个原因叫它trunk。不幸的是,如果没有(1)您的代码和确切的配置(2)在相同的体系结构上具有相同的GCC版本(3),则几乎不可能分辨出问题所在。我建议检查当10.0.1从干线变为稳定状态时是否仍然存在。
Marco Bonelli

1
还有一条评论:-mtune=native针对您的计算机具有的特定CPU进行优化。对于不同的测试人员而言,情况将有所不同,并且可能是问题的一部分。如果与您一起运行编译,则-v应该能够看到计算机上(例如-mtune=skylake,我的计算机上)哪个cpu系列。
Nate Eldredge

1
从调试运行中仍然很难分辨。拆卸应是结论性的。您无需提取任何内容,只需找到在编译项目并反汇编时生成的.o文件即可。您也可以使用disassemblegdb中的指令。
Nate Eldredge

5
无论如何,恭喜,您是少数几个问题实际上是编译器错误的人之一。
Nate Eldredge

Answers:


22

简介:这似乎是gcc中的错误,与字符串优化有关。下面是一个自包含的测试用例。最初人们对代码是否正确表示怀疑,但我认为是正确的。

我已经将该错误报告为PR 93982已提交了建议的修复程序,但并未在所有情况下都进行修复,从而导致了后续问题PR 94015godbolt链接)。

通过使用flag进行编译,您应该能够解决该错误-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 trunk(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))。

有趣的玩代码,看看是什么影响了“ bug”。可能很重要的是,将的返回类型更改R_allocvoid *可以使其消失,并为您提供“正确”的程序集输出。可能不太重要但更有趣的是,将字符串更改"12345678"为更长或更短也会使它消失。


先前的讨论现已解决-该代码显然合法。

我的问题是您的代码是否真正合法。您采用char *return by R_alloc()并将其强制转换为const char **,然后存储a 的事实const char *似乎违反了严格的别名规则,因为charconst char *不是兼容类型。有一个例外允许您访问任何对象char(以实现memcpy),但这是相反的方式,并且据我所知,这是不允许的。它使您的代码产生未定义的行为,因此编译器可以合法地执行所需的任何操作。

如果是这样,则正确的解决方法是让R更改其代码,以便R_alloc()返回void *而不是char *。这样就不会出现混叠问题。不幸的是,该代码不在您的控制范围之内,而且我不清楚如何在不违反严格别名的情况下完全使用此功能。解决方法可能是插入一个临时变量,例如void *tmp = R_alloc(); res.target = tmp;,它可以解决测试用例中的问题,但是我仍然不确定它是否合法。

但是,我不知道这个“严格走样”的假设,因为编制-fno-strict-aliasing,据我所知这应该使海湾合作委员会允许这样的构造,并没有使问题消失!


更新。尝试一些不同的选择,我发现无论是-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来自a的大小char *。也就是说,就好像它只是在address存储一个nul字节一样res.target+8。但是,编译器“知道”先前的语句已经在该地址存储了一个nul字节!因此,该语句是“冗余的”,可以被丢弃(此处)。

这解释了为什么字符串必须正好是8个字符长才能触发错误。(尽管其他8的倍数在其他情况下也会触发该错误。)


FWIW重铸到另一种类型的指针已记录在案。我不知道混叠是否知道可以重铸,int*但不能重铸const char**
BrodieG

如果我对严格的别名的理解是正确的,则强制转换int *为也是非法的(或者实际上,将ints在那里存储是非法的)。
Nate Eldredge

1
这与严格的别名规则无关。严格的别名规则是关于使用不同的句柄访问存储的数据。正如您仅在此处分配的那样,它不涉及严格的别名规则。当两种类型的指针都具有相同的对齐要求时,强制转换指针有效,但是在这里,您char*正在使用x86_64 进行转换并在其中工作...在这里我看不到UB,这是gcc错误。
KamilCuk

1
是的也没有,@ KamilCuk。在标准的术语中,“访问”包括读取和修改对象的值。因此,严格的别名规则确实会涉及“存储”。它不限于回读操作。但是对于没有声明类型的对象,要解决的事实是,写入这样的对象会自动更改其有效类型以对应于所写入的内容。没有声明类型的对象就是动态分配的对象(无论访问它们的指针的类型如何),因此这里确实没有违反SA的情况。
John Bollinger

2
是的,@ Nate,使用的定义R_alloc(),无论程序定义了哪个翻译单位,该程序都符合R_alloc()。正是编译器在此处不符合要求。
John Bollinger
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.