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"
이 시점에서 컴파일 된 코드는 다음 라인을 따라 즉시 segfaulting을 시작했습니다.
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
최적화 수준 의 rocker/r-base
도커 컨테이너를 사용하여 segfault를 일관되게 재현 할 수있었습니다 . 낮은 최적화를 실행하면 문제가 해결됩니다. valgrind (-O0 및 -O2), UBSAN (gcc / clang)을 포함하여 다른 설정을 실행해도 전혀 문제가 없습니다. 나는 또한 이것이 아래에 있다고 확신 하지만 데이터가 없습니다.gcc-10.0.1
-O2
gcc-10.0.0
나는 gcc-10.0.1 -O2
버전을 사용 gdb
하여 나에게 이상한 것으로 나타났습니다.
강조 표시된 섹션을 단계별로 실행하는 동안 배열의 두 번째 요소 초기화가 생략 된 것으로 나타납니다 ( R로 제어를 리턴 할 때 자체 가비지가 수집 R_alloc
하는 래퍼 malloc
입니다. segfault는 R로 리턴하기 전에 발생합니다). 나중에 초기화되지 않은 요소 (gcc.10.0.1 -O2 버전)에 액세스하면 프로그램이 충돌합니다.
코드의 모든 위치에서 문제의 요소를 명시 적으로 초기화 하여이 문제를 해결했지만 결국 요소를 사용했지만 실제로는 빈 문자열로 초기화되었거나 적어도 내가 생각했던 것입니다.
내가 명백한 것을 놓치고 있거나 어리석은 짓을하고 있습니까? 모두 합리적 가능성이 C 나의 두 번째 언어입니다 같습니다 까지 . 이것이 방금 자랐다는 것이 이상합니다. 컴파일러가 무엇을하려고하는지 알 수 없습니다.
업데이트 : debian:testing
도커 컨테이너가에 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
초기화되지 않은 문자열에 액세스하려고 하는 (정확히 이해하면) 매우 빨리 표시됩니다
.
업데이트 2 : 이것은 매우 재귀 적 인 함수이며 문자열 초기화 비트는 여러 번 호출됩니다. 이것은 주로 게으른 b / c입니다. 재귀에서보고하려는 것이 실제로 발생하는 한 번만 초기화 된 문자열 만 필요하지만 무언가가 발생할 때마다 초기화하는 것이 더 쉽습니다. 다음에 보게 될 것은 다중 초기화를 보여 주지만, 그중 하나 (아마도 <0x1400000001> 인 것) 만 사용되고 있기 때문에 이것을 언급했습니다.
여기에 표시하는 내용이 segfault를 유발 한 요소와 직접 관련이 있음을 보장 할 수는 없지만 (같은 불법 주소 액세스 임에도 불구하고) @ 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 draft ).
6.3.2.3 Par7 변환> 기타 피연산자> 포인터
객체 유형에 대한 포인터는 다른 객체 유형에 대한 포인터로 변환 될 수 있습니다. 참조 된 유형에 대해 결과 포인터가 올바르게 정렬되지 않으면 68) 동작이 정의되지 않습니다.
그렇지 않으면, 다시 변환 될 때 결과는 원래 포인터와 동일하게 비교됩니다. 객체에 대한 포인터가 문자 유형에 대한 포인터로 변환되면 결과는 객체의 주소가 가장 낮은 바이트를 가리 킵니다. 객체의 크기까지 결과의 연속적인 증가는 객체의 나머지 바이트에 대한 포인터를 생성합니다.
6.5 Par6 표현식
저장된 값에 액세스하기위한 유효 오브젝트 유형은 선언 된 오브젝트 유형입니다 (있는 경우). 87) 문자 유형이 아닌 유형을 가진 lvalue를 통해 선언 된 유형이없는 객체에 값이 저장되면, lvalue의 유형은 해당 액세스 및 이후의 액세스에 대한 객체의 유효 유형이됩니다. 저장된 값을 수정하십시오. memcpy 또는 memmove를 사용하여 선언 된 유형이없는 객체에 값을 복사하거나 문자 유형의 배열로 복사하는 경우 해당 액세스 및 값을 수정하지 않는 후속 액세스에 대해 수정 된 객체의 유효 유형은 다음과 같습니다. 값이있는 경우 오브젝트의 유효 유형 (있는 경우) 선언 된 유형이없는 객체에 대한 다른 모든 액세스의 경우 객체의 유효 유형은 단순히 액세스에 사용 된 lvalue의 유형입니다.
87) 할당 된 객체는 선언 된 타입이 없다.
IIUC R_alloc
는 정렬 malloc
이 보장 된 ed 블록 으로 오프셋을 반환하고 오프셋 double
이후의 블록 크기는 요청 된 크기입니다 (R 특정 데이터에 대한 오프셋 전에 할당도 있음). 리턴시 R_alloc
포인터를 캐스트합니다 (char *)
.
섹션 6.2.5 파 29
void에 대한 포인터는 문자 유형에 대한 포인터와 동일한 표현 및 정렬 요구 사항을 가져야합니다. 48) 마찬가지로, 적격 또는 비 적격 버전의 호환 가능한 유형에 대한 포인터는 동일한 표현 및 정렬 요구 사항을 가져야한다. 구조 유형에 대한 모든 포인터는 서로 동일한 표현 및 정렬 요구 사항을 가져야합니다.
공용체 유형에 대한 모든 포인터는 서로 동일한 표현 및 정렬 요구 사항을 가져야합니다.
다른 유형에 대한 포인터는 동일한 표현 또는 정렬 요구 사항을 가질 필요는 없습니다.48) 동일한 표현 및 정렬 요구 사항은 함수에 대한 인수, 함수의 반환 값 및 조합 멤버를 상호 교환 할 수 있음을 의미합니다.
질문은 그래서 "우리는 개작 할 수 있습니다 (char *)
에 (const char **)
로와 쓰기 (const char **)
". 위의 내용은 코드가 실행되는 시스템의 포인터가 정렬과 호환되는 double
정렬이면 괜찮습니다.
"엄격한 앨리어싱"을 위반하고 있습니까? 즉 :
6.5 파 7
객체는 다음 유형 중 하나를 갖는 lvalue 표현식에 의해서만 저장된 값에 액세스해야합니다.
— 객체의 유효 유형과 호환되는 유형 ...
88)이 목록의 목적은 객체가 별칭을 가질 수도 있고 그렇지 않을 수도있는 환경을 지정하는 것입니다.
그렇다면 컴파일러 는 (또는 )이 가리키는 객체 의 유효 유형 이 무엇이라고 생각해야 합니까? 아마도 선언 된 type 입니까, 아니면 실제로 모호합니까? 범위에 동일한 객체에 액세스하는 다른 'lvalue'가 없기 때문에이 경우에만 해당되지 않는다고 생각합니다.res.target
res.current
(const char **)
나는 표준의이 부분들에서 의미를 추출하기 위해 힘 쓰고있다.
-mtune=native
머신이 가지고있는 특정 CPU를 최적화합니다. 테스터마다 다를 수 있으며 문제의 일부일 수 있습니다. 컴파일을 실행하면 -v
컴퓨터 (예 : -mtune=skylake
내 컴퓨터)에 있는 CPU 제품군을 볼 수 있어야합니다 .
disassemble
gdb 내 에서 명령을 사용할 수도 있습니다 .