Tôi có một gói R với mã được biên dịch C tương đối ổn định trong một thời gian và thường được thử nghiệm trên nhiều nền tảng và trình biên dịch (windows / osx / debian / fedora gcc / clang).
Gần đây, một nền tảng mới đã được thêm vào để kiểm tra lại gói:
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"
Tại thời điểm đó, mã được biên dịch nhanh chóng bắt đầu segfaulting dọc theo các dòng này:
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
Tôi đã có thể tái tạo segfault một cách nhất quán bằng cách sử dụng bộ rocker/r-base
chứa docker gcc-10.0.1
với mức tối ưu hóa -O2
. Chạy tối ưu hóa thấp hơn được thoát khỏi vấn đề. Chạy bất kỳ thiết lập nào khác, bao gồm dưới valgrind (cả -O0 và -O2), UBSAN (gcc / clang), cho thấy không có vấn đề gì cả. Tôi cũng chắc chắn rằng điều này chạy theo gcc-10.0.0
, nhưng không có dữ liệu.
Tôi đã chạy gcc-10.0.1 -O2
phiên bản với gdb
và nhận thấy một cái gì đó có vẻ kỳ lạ đối với tôi:
Trong khi bước qua phần được tô sáng, có vẻ như việc khởi tạo các phần tử thứ hai của các mảng bị bỏ qua ( R_alloc
là một trình bao bọc xung quanh malloc
đó rác tự thu thập khi trả lại quyền điều khiển cho R; segfault xảy ra trước khi trở về R). Sau đó, chương trình gặp sự cố khi phần tử chưa khởi tạo (trong phiên bản gcc.10.0.1 -O2) được truy cập.
Tôi đã sửa lỗi này bằng cách khởi tạo rõ ràng phần tử được đề cập ở mọi nơi trong mã mà cuối cùng dẫn đến việc sử dụng phần tử, nhưng nó thực sự đã được khởi tạo thành một chuỗi trống, hoặc ít nhất đó là những gì tôi sẽ giả định.
Tôi đang thiếu một cái gì đó rõ ràng hoặc làm điều gì đó ngu ngốc? Cả hai đều có khả năng hợp lý vì C là ngôn ngữ thứ hai của tôi cho đến nay . Thật kỳ lạ khi điều này vừa được cắt ra và tôi không thể hiểu trình biên dịch đang cố gắng làm gì.
CẬP NHẬT : Hướng dẫn sao chép này, mặc dù điều này sẽ chỉ sao chép miễn là debian:testing
container docker có gcc-10
tại gcc-10.0.1
. Ngoài ra, đừng chỉ chạy các lệnh này nếu bạn không tin tưởng tôi .
Xin lỗi đây không phải là một ví dụ tái sản xuất tối thiểu.
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
Sau đó, trong R console, sau khi gõ run
để có được gdb
để chạy các chương trình:
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)
Kiểm tra trong gdb khá nhanh cho thấy (nếu tôi hiểu chính xác)
CSR_strmlen_x
đang cố truy cập chuỗi không được khởi tạo.
CẬP NHẬT 2 : đây là một hàm đệ quy cao và trên hết, bit khởi tạo chuỗi được gọi là nhiều, nhiều lần. Điều này chủ yếu là vì tôi lười biếng, chúng tôi chỉ cần các chuỗi được khởi tạo cho một lần chúng tôi thực sự gặp phải điều gì đó mà chúng tôi muốn báo cáo trong đệ quy, nhưng sẽ dễ dàng hơn để khởi tạo mỗi khi có thể gặp phải điều gì đó. Tôi đề cập đến điều này bởi vì những gì bạn sẽ thấy tiếp theo hiển thị nhiều lần khởi tạo, nhưng chỉ một trong số chúng (có lẽ là địa chỉ có địa chỉ <0x1400000001>) đang được sử dụng.
Tôi không thể đảm bảo rằng những thứ tôi hiển thị ở đây có liên quan trực tiếp đến yếu tố gây ra segfault (mặc dù đó là cùng một địa chỉ bất hợp pháp), nhưng như @ nate-eldredge đã hỏi rằng nó không phải là yếu tố mảng khởi tạo ngay trước khi trả về hoặc ngay sau khi trả về trong chức năng gọi. Lưu ý chức năng gọi điện đang khởi tạo 8 trong số này và tôi hiển thị tất cả, với tất cả chúng chứa đầy rác hoặc bộ nhớ không thể truy cập.
CẬP NHẬT 3 , tháo gỡ chức năng trong câu hỏi:
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.
CẬP NHẬT 4 :
Vì vậy, cố gắng phân tích thông qua tiêu chuẩn ở đây là những phần có vẻ phù hợp ( dự thảo C11 ):
6.3.2.3 Chuyển đổi Par7> Toán tử khác> Con trỏ
Một con trỏ tới một loại đối tượng có thể được chuyển đổi thành một con trỏ thành một loại đối tượng khác. Nếu con trỏ kết quả không được căn chỉnh chính xác 68) cho loại được tham chiếu, hành vi không được xác định.
Mặt khác, khi được chuyển đổi trở lại một lần nữa, kết quả sẽ so sánh bằng với con trỏ ban đầu. Khi một con trỏ tới một đối tượng được chuyển đổi thành một con trỏ thành một kiểu ký tự, kết quả sẽ trỏ đến byte có địa chỉ thấp nhất của đối tượng. Gia tăng liên tiếp của kết quả, cho đến kích thước của đối tượng, mang lại con trỏ tới các byte còn lại của đối tượng.
6.5 Biểu thức Par6
Loại hiệu quả của một đối tượng để truy cập vào giá trị được lưu trữ của nó là loại khai báo của đối tượng, nếu có. 87) Nếu một giá trị được lưu trữ vào một đối tượng không có kiểu khai báo thông qua một giá trị có loại không phải là kiểu ký tự, thì loại giá trị đó trở thành loại đối tượng hiệu quả cho truy cập đó và cho các lần truy cập tiếp theo không sửa đổi giá trị được lưu trữ. Nếu một giá trị được sao chép vào một đối tượng không có kiểu khai báo sử dụng memcpy hoặc memmove hoặc được sao chép dưới dạng một mảng kiểu ký tự, thì loại hiệu quả của đối tượng được sửa đổi cho truy cập đó và cho các truy cập tiếp theo không sửa đổi giá trị là loại hiệu quả của đối tượng mà từ đó giá trị được sao chép, nếu nó có một. Đối với tất cả các truy cập khác vào một đối tượng không có loại khai báo, loại đối tượng hiệu quả chỉ đơn giản là loại giá trị được sử dụng cho truy cập.
87) Các đối tượng được phân bổ không có loại khai báo.
IIUC R_alloc
trả về phần bù vào malloc
khối ed được đảm bảo double
căn chỉnh và kích thước của khối sau phần bù có kích thước được yêu cầu (cũng có phân bổ trước phần bù cho dữ liệu cụ thể R). R_alloc
phôi mà con trỏ (char *)
trở lại.
Mục 6.2.5 Par 29
Một con trỏ đến void sẽ có cùng các yêu cầu biểu diễn và căn chỉnh như một con trỏ tới một kiểu ký tự. 48) Tương tự, các con trỏ tới các phiên bản đủ điều kiện hoặc không đủ tiêu chuẩn của các loại tương thích sẽ có cùng các yêu cầu về đại diện và căn chỉnh. Tất cả các con trỏ đến các loại cấu trúc sẽ có cùng các yêu cầu đại diện và căn chỉnh như nhau.
Tất cả các con trỏ tới các loại kết hợp sẽ có cùng các yêu cầu đại diện và căn chỉnh như nhau.
Con trỏ đến các loại khác không cần phải có cùng yêu cầu đại diện hoặc căn chỉnh.48) Các yêu cầu về biểu diễn và căn chỉnh giống nhau có nghĩa là ngụ ý các khả năng thay thế cho các chức năng, trả về các giá trị từ các chức năng và các thành viên của các hiệp hội.
Vì vậy, câu hỏi là "chúng ta được phép viết lại các (char *)
đến (const char **)
và ghi vào nó như là (const char **)
". Tôi đọc ở trên là miễn là con trỏ trên các hệ thống mà mã chạy trong có căn chỉnh tương thích với double
căn chỉnh, thì không sao.
Có phải chúng ta đang vi phạm "răng cưa nghiêm ngặt"? I E:
6,5 mệnh 7
Một đối tượng sẽ có giá trị được lưu trữ chỉ được truy cập bởi một biểu thức giá trị có một trong các loại sau: 88)
- một loại tương thích với loại hiệu quả của đối tượng ...
88) Mục đích của danh sách này là chỉ định những trường hợp trong đó một đối tượng có thể hoặc không thể được đặt bí danh.
Vì vậy, trình biên dịch nên nghĩ loại hiệu quả của đối tượng được trỏ bởi res.target
(hoặc res.current
) là gì? Có lẽ là loại khai báo (const char **)
, hoặc điều này thực sự mơ hồ? Tôi cảm thấy rằng nó không chỉ trong trường hợp này bởi vì không có "giá trị" nào khác trong phạm vi truy cập vào cùng một đối tượng.
Tôi sẽ thừa nhận tôi đang vật lộn để rút ra ý nghĩa từ các phần của tiêu chuẩn này.
-mtune=native
tối ưu hóa cho CPU cụ thể mà máy của bạn có. Điều đó sẽ khác nhau đối với những người thử nghiệm khác nhau và có thể là một phần của vấn đề. Nếu bạn chạy phần biên dịch với -v
bạn, bạn sẽ có thể thấy họ cpu nào trên máy của bạn (ví dụ: -mtune=skylake
trên máy tính của tôi).
disassemble
hướng dẫn bên trong gdb.