Saya punya paket R dengan kode kompilasi C yang sudah relatif stabil untuk sementara waktu dan sering diuji terhadap berbagai platform dan kompiler (windows / osx / debian / fedora gcc / clang).
Baru-baru ini platform baru ditambahkan untuk menguji paket lagi:
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"
Pada titik mana kode yang dikompilasi segera mulai melakukan segmentasi di sepanjang baris berikut:
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
Saya dapat mereproduksi segfault secara konsisten dengan menggunakan rocker/r-base
wadah buruh pelabuhan gcc-10.0.1
dengan tingkat pengoptimalan -O2
. Menjalankan optimasi yang lebih rendah menghilangkan masalah. Menjalankan set-up lainnya, termasuk di bawah valgrind (keduanya -O0 dan -O2), UBSAN (gcc / clang), tidak menunjukkan masalah sama sekali. Saya juga cukup yakin ini berjalan di bawah gcc-10.0.0
, tetapi tidak memiliki data.
Saya menjalankan gcc-10.0.1 -O2
versi itu gdb
dan memperhatikan sesuatu yang terasa aneh bagi saya:
Sementara melangkah melalui bagian yang disorot tampak inisialisasi elemen kedua array dilewati ( R_alloc
adalah pembungkus di sekitar malloc
pengumpulan sampah sendiri ketika mengembalikan kontrol ke R; segfault terjadi sebelum kembali ke R). Kemudian, program macet ketika elemen yang tidak diinisialisasi (dalam versi gcc.10.0.1 -O2) diakses.
Saya memperbaikinya dengan menginisialisasi elemen secara eksplisit di mana-mana dalam kode yang akhirnya mengarah ke penggunaan elemen, tetapi itu seharusnya benar-benar diinisialisasi ke string kosong, atau setidaknya itulah yang akan saya asumsikan.
Apakah saya kehilangan sesuatu yang jelas atau melakukan sesuatu yang bodoh? Keduanya cukup masuk akal karena C adalah bahasa kedua saya sejauh ini . Hanya aneh bahwa ini baru saja dipotong sekarang, dan saya tidak tahu apa yang coba dilakukan oleh kompiler.
UPDATE : Petunjuk untuk mereproduksi ini, meskipun ini hanya akan mereproduksi selama debian:testing
wadah buruh pelabuhan memiliki gcc-10
di gcc-10.0.1
. Juga, jangan hanya menjalankan perintah ini jika Anda tidak percaya padaku .
Maaf ini bukan contoh minimal yang dapat direproduksi.
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
Kemudian di konsol R, setelah mengetik run
untuk mendapatkan gdb
untuk menjalankan program:
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)
Memeriksa di gdb menunjukkan cukup cepat (jika saya mengerti benar) yang
CSR_strmlen_x
mencoba mengakses string yang tidak diinisialisasi.
UPDATE 2 : ini adalah fungsi yang sangat rekursif, dan di atas itu bit inisialisasi string dipanggil berkali-kali. Ini sebagian besar b / c saya sedang malas, kita hanya perlu string diinisialisasi untuk satu kali kita benar-benar menemukan sesuatu yang ingin kita laporkan dalam rekursi, tetapi lebih mudah untuk menginisialisasi setiap kali mungkin untuk menemukan sesuatu. Saya menyebutkan ini karena apa yang akan Anda lihat selanjutnya menunjukkan beberapa inisialisasi, tetapi hanya satu dari mereka (mungkin yang dengan alamat <0x1400000001>) sedang digunakan.
Saya tidak dapat menjamin bahwa hal-hal yang saya perlihatkan di sini secara langsung berkaitan dengan elemen yang menyebabkan segfault (meskipun itu adalah akses alamat ilegal yang sama), tetapi ketika @ nate-eldredge bertanya, itu menunjukkan bahwa elemen array tidak diinisialisasi baik sebelum kembali atau tepat setelah kembali dalam fungsi panggilan. Perhatikan fungsi panggilan menginisialisasi 8 dari ini, dan saya tunjukkan semuanya, dengan semuanya dipenuhi dengan sampah atau memori yang tidak dapat diakses.
UPDATE 3 , pembongkaran fungsi yang dimaksud:
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.
PEMBARUAN 4 :
Jadi, mencoba mengurai melalui standar di sini adalah bagian-bagiannya yang tampaknya relevan ( draft C11 ):
6.3.2.3 Konversi Par7> Operan Lain> Pointer
Pointer ke tipe objek dapat dikonversi ke pointer ke tipe objek yang berbeda. Jika pointer yang dihasilkan tidak sejajar dengan benar ( 68) untuk tipe yang direferensikan, perilaku tidak terdefinisi.
Kalau tidak, ketika dikonversi kembali lagi, hasilnya harus membandingkan sama dengan pointer asli. Ketika pointer ke objek dikonversi ke pointer ke tipe karakter, hasilnya menunjuk ke byte terendah dari objek. Peningkatan hasil berturut-turut, hingga ukuran objek, menghasilkan pointer ke byte tersisa dari objek.
6.5 Par6 Ekspresi
Jenis objek yang efektif untuk akses ke nilai yang disimpannya adalah tipe objek yang dinyatakan, jika ada. 87) Jika suatu nilai disimpan ke dalam objek yang tidak memiliki tipe yang dideklarasikan melalui nilai lv yang memiliki tipe yang bukan tipe karakter, maka tipe nilai tersebut menjadi tipe efektif objek untuk akses itu dan untuk akses selanjutnya yang tidak. ubah nilai yang disimpan. Jika nilai disalin ke objek yang tidak memiliki tipe yang dideklarasikan menggunakan memcpy atau memmove, atau disalin sebagai array tipe karakter, maka tipe efektif objek yang dimodifikasi untuk akses tersebut dan untuk akses selanjutnya yang tidak mengubah nilai adalah jenis objek yang efektif dari mana nilai disalin, jika ada. Untuk semua akses lainnya ke objek yang tidak memiliki tipe yang dideklarasikan, tipe efektif objek hanyalah tipe nilai yang digunakan untuk akses.
87) Objek yang dialokasikan tidak memiliki tipe yang dideklarasikan.
IIUC R_alloc
mengembalikan offset ke malloc
blok ed yang dijamin akan double
selaras, dan ukuran blok setelah offset adalah dari ukuran yang diminta (ada juga alokasi sebelum offset untuk data spesifik R). R_alloc
melemparkan penunjuk itu ke belakang (char *)
.
Bagian 6.2.5 Par 29
Penunjuk untuk membatalkan harus memiliki persyaratan representasi dan perataan yang sama seperti penunjuk ke tipe karakter. 48) Demikian pula, pointer ke versi yang memenuhi syarat atau tidak memenuhi syarat dari jenis yang kompatibel harus memiliki persyaratan representasi dan perataan yang sama. Semua pointer ke tipe struktur harus memiliki persyaratan representasi dan perataan yang sama satu sama lain.
Semua pointer ke tipe-tipe serikat pekerja harus memiliki persyaratan representasi dan penyelarasan yang sama satu sama lain.
Pointer ke tipe lain tidak perlu memiliki representasi atau persyaratan penyelarasan yang sama.48) Persyaratan representasi dan penyelarasan yang sama dimaksudkan untuk menyiratkan argumen yang dapat dipertukarkan ke fungsi, mengembalikan nilai dari fungsi, dan anggota serikat pekerja.
Jadi pertanyaannya adalah "apakah kita diizinkan untuk menyusun kembali (char *)
ke (const char **)
dan menulis sebagai (const char **)
". Bacaan saya di atas adalah bahwa selama pointer pada sistem kode dijalankan memiliki keselarasan yang kompatibel dengan double
penyelarasan, maka tidak apa-apa.
Apakah kita melanggar "aliasing ketat"? yaitu:
6.5 Par 7
Objek harus memiliki nilai tersimpan diakses hanya oleh ekspresi lvalue yang memiliki salah satu dari jenis berikut: 88)
- jenis yang kompatibel dengan jenis objek yang efektif ...
88) Maksud dari daftar ini adalah untuk menentukan keadaan-keadaan di mana suatu objek mungkin atau mungkin tidak disebutkan.
Jadi, apa yang seharusnya dikompilasi oleh kompilator jenis objek yang ditunjuk oleh res.target
(atau res.current
)? Agaknya tipe yang dideklarasikan (const char **)
, atau apakah ini sebenarnya ambigu? Rasanya bagi saya bahwa tidak dalam kasus ini hanya karena tidak ada 'nilai' lain dalam ruang lingkup yang mengakses objek yang sama.
Saya akui saya berjuang keras untuk mengeluarkan akal dari bagian-bagian standar ini.
sumber
-mtune=native
mengoptimalkan untuk CPU tertentu yang dimiliki mesin Anda. Itu akan berbeda untuk penguji yang berbeda dan mungkin menjadi bagian dari masalah. Jika Anda menjalankan kompilasi dengan-v
Anda harus dapat melihat keluarga cpu mana yang ada di mesin Anda (misalnya-mtune=skylake
di komputer saya).disassemble
instruksi di dalam gdb.Jawaban:
Rangkuman: Ini tampaknya merupakan bug dalam gcc, terkait dengan optimasi string. Testcase mandiri ada di bawah ini. Awalnya ada beberapa keraguan apakah kode itu benar, tapi saya pikir itu benar.
Saya telah melaporkan bug sebagai PR 93982 . Perbaikan yang diusulkan telah dilakukan tetapi tidak memperbaikinya dalam semua kasus, mengarah ke tindak lanjut PR 94015 ( tautan godbolt ).
Anda harus dapat mengatasi bug dengan kompilasi dengan bendera
-fno-optimize-strlen
.Saya dapat mengurangi test case Anda menjadi contoh minimal berikut (juga pada godbolt ):
Dengan gcc trunk (gcc versi 10.0.1 20200225 (percobaan)) dan
-O2
(semua opsi lain ternyata tidak diperlukan), rakitan yang dihasilkan pada amd64 adalah sebagai berikut:Jadi Anda benar bahwa kompiler gagal diinisialisasi
res.target[1]
(perhatikan tidak adanya yang mencolok darimovq $.LC1, 8(%rax)
).Sangat menarik untuk bermain dengan kode dan melihat apa yang mempengaruhi "bug". Mungkin secara signifikan, mengubah tipe kembali
R_alloc
untukvoid *
membuatnya hilang, dan memberi Anda output perakitan yang "benar". Mungkin kurang signifikan tetapi lebih menyenangkan, mengubah string"12345678"
menjadi lebih panjang atau lebih pendek juga membuatnya hilang.Diskusi sebelumnya, sekarang diselesaikan - kode ini tampaknya sah.
Pertanyaan saya adalah apakah kode Anda benar-benar legal. Fakta bahwa Anda mengambil yang
char *
dikembalikan olehR_alloc()
dan melemparkannya keconst char **
, dan kemudian menyimpanconst char *
sepertinya itu mungkin melanggar aturan aliasing yang ketat , karenachar
danconst char *
bukan tipe yang kompatibel. Ada pengecualian yang memungkinkan Anda untuk mengakses objek apa pun sebagaichar
(untuk mengimplementasikan hal-hal sepertimemcpy
), tetapi ini sebaliknya, dan sejauh yang saya mengerti, itu tidak diperbolehkan. Itu membuat kode Anda menghasilkan perilaku yang tidak terdefinisi sehingga kompiler dapat secara legal melakukan apa pun yang diinginkannya.Jika demikian, memperbaiki yang benar akan untuk R untuk mengubah kode mereka sehingga
R_alloc()
kembalivoid *
bukanchar *
. Maka tidak akan ada masalah alias. Sayangnya, kode itu berada di luar kendali Anda, dan tidak jelas bagi saya bagaimana Anda dapat menggunakan fungsi ini sama sekali tanpa melanggar alias ketat. Solusi mungkin untuk menempatkan variabel sementara, misalnyavoid *tmp = R_alloc(); res.target = tmp;
yang memecahkan masalah dalam kasus uji, tapi saya masih tidak yakin apakah itu legal.Namun, saya tidak yakin dengan hipotesis "aliasing ketat" ini, karena mengkompilasi dengan
-fno-strict-aliasing
, yang mana AFAIK seharusnya membuat gcc mengizinkan konstruksi semacam itu, tidak membuat masalah hilang!Memperbarui. Mencoba beberapa opsi berbeda, saya menemukan bahwa salah satu
-fno-optimize-strlen
atau-fno-tree-forwprop
akan menghasilkan "benar" kode yang dihasilkan. Juga, menggunakan-O1 -foptimize-strlen
menghasilkan kode yang salah (tetapi-O1 -ftree-forwprop
tidak).Setelah sedikit
git bisect
latihan, kesalahan tampaknya telah diperkenalkan di commit 34fcf41e30ff56155e996f5e04 .Pembaruan 2. Saya mencoba menggali ke sumber gcc sedikit, hanya untuk melihat apa yang bisa saya pelajari. (Saya tidak mengklaim sebagai ahli kompiler apa pun!)
Sepertinya kode di
tree-ssa-strlen.c
dimaksudkan untuk melacak string yang muncul di program. Sejauh yang saya tahu, bug adalah bahwa dalam melihat pernyataanres.target[0] = "12345678";
kompiler mengonfigurasi alamat string literal"12345678"
dengan string itu sendiri. (Itu tampaknya terkait dengan kode mencurigakan ini yang ditambahkan dalam komit yang disebutkan di atas, di mana jika ia mencoba untuk menghitung byte dari sebuah "string" yang sebenarnya merupakan alamat, ia malah melihat apa yang ditunjukkan oleh alamat itu.)Jadi berpikir bahwa pernyataan
res.target[0] = "12345678"
, bukan menyimpan alamat dari"12345678"
di alamatres.target
, adalah menyimpan string itu sendiri di alamat itu, seperti jika pernyataan itustrcpy(res.target, "12345678")
. Catatan untuk apa yang ada di depan bahwa ini akan mengakibatkan trailing nul disimpan di alamatres.target+8
(pada tahap ini di kompiler, semua offset dalam byte).Sekarang ketika kompilator melihat
res.target[1] = ""
, ia juga memperlakukan ini seolah-olah itustrcpy(res.target+8, "")
, 8 berasal dari ukuran achar *
. Artinya, seolah-olah itu hanya menyimpan byte byte di alamatres.target+8
. Namun, kompiler "tahu" bahwa pernyataan sebelumnya sudah menyimpan byte di alamat itu! Dengan demikian, pernyataan ini "berlebihan" dan dapat dibuang (di sini ).Ini menjelaskan mengapa string harus panjangnya 8 karakter untuk memicu bug. (Meskipun kelipatan 8 lainnya juga dapat memicu bug dalam situasi lain.)
sumber
int*
boleh kembali tetapi tidakconst char**
.int *
juga ilegal (atau lebih tepatnya, benar-benar menyimpanint
di sana ilegal).char*
dan bekerja pada x86_64 ... Saya tidak melihat UB di sini, ini bug gcc.R_alloc()
, programnya sudah sesuai, terlepas dari unit terjemahan mana yangR_alloc()
didefinisikan. Ini adalah kompiler yang gagal untuk menyesuaikan di sini.