我有一个带有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-base
docker容器,我能够始终如一地再现段错误。运行较低的优化可以解决该问题。运行任何其他设置,包括在valgrind(在-O0和-O2下),UBSAN(gcc / clang)下,都没有任何问题。我也有理由确定这是根据,但没有数据。gcc-10.0.1
-O2
gcc-10.0.0
我使用来运行该gcc-10.0.1 -O2
版本,gdb
并发现对我来说似乎有些奇怪:
逐步执行突出显示的部分时,似乎跳过了数组第二个元素的初始化(R_alloc
是malloc
将控制权返回给R时自垃圾回收的包装;分段故障发生在返回R之前)。后来,当访问未初始化的元素(在gcc.10.0.1 -O2版本中)时,程序崩溃。
我通过在代码的各处显式初始化有问题的元素(最终导致该元素的使用)来解决此问题,但实际上应该已将其初始化为空字符串,或者至少这就是我所假设的。
我是否缺少明显的东西或做愚蠢的事情?到目前为止,由于C是我的第二语言,所以两者都有可能。奇怪的是,现在这个问题突然出现了,我不知道编译器在试图做什么。
更新:重现此指令,尽管仅在debian:testing
Docker容器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>的初始化)。
我不能保证我在这里显示的内容与导致段错误的元素直接相关(尽管这是相同的非法地址访问),但是正如@ nate-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.target
res.current
(const char **)
我承认我正在竭尽全力从标准的这些部分中提取意义。
-mtune=native
针对您的计算机具有的特定CPU进行优化。对于不同的测试人员而言,情况将有所不同,并且可能是问题的一部分。如果与您一起运行编译,则-v
应该能够看到计算机上(例如-mtune=skylake
,我的计算机上)哪个cpu系列。
disassemble
gdb中的指令。