Ho un pacchetto R con codice compilato in C che è stato relativamente stabile per un po 'ed è spesso testato su un'ampia varietà di piattaforme e compilatori (windows / osx / debian / fedora gcc / clang).
Più recentemente è stata aggiunta una nuova piattaforma per testare nuovamente il pacchetto:
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"
A quel punto il codice compilato ha subito iniziato a segfaulting seguendo queste linee:
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
Sono stato in grado di riprodurre il segfault in modo coerente utilizzando il rocker/r-base
contenitore docker gcc-10.0.1
con livello di ottimizzazione -O2
. L'esecuzione di un'ottimizzazione inferiore elimina il problema. Eseguendo qualsiasi altro set-up, incluso sotto valgrind (sia -O0 che -O2), UBSAN (gcc / clang), non mostra alcun problema. Sono anche ragionevolmente sicuro che questo abbia funzionato gcc-10.0.0
, ma non ho i dati.
Ho eseguito la gcc-10.0.1 -O2
versione con gdb
e ho notato qualcosa che mi sembra strano:
Mentre si passa attraverso la sezione evidenziata, sembra che l'inizializzazione dei secondi elementi delle matrici sia saltata ( R_alloc
è un involucro attorno a malloc
quel self garbage che si raccoglie quando si restituisce il controllo a R; il segfault avviene prima di tornare a R). Successivamente, il programma si arresta in modo anomalo quando si accede all'elemento non inizializzato (nella versione gcc.10.0.1 -O2).
Ho risolto questo problema inizializzando esplicitamente l'elemento in questione ovunque nel codice che alla fine ha portato all'utilizzo dell'elemento, ma in realtà avrebbe dovuto essere inizializzato su una stringa vuota, o almeno è quello che avrei ipotizzato.
Mi sto perdendo qualcosa di ovvio o sto facendo qualcosa di stupido? Entrambi sono ragionevolmente probabili poiché C è di gran lunga la mia seconda lingua . È strano che questo sia appena spuntato ora, e non riesco a capire cosa sta cercando di fare il compilatore.
UPDATE : Le istruzioni per riprodurre questo, anche se questo riprodurrà solo finché debian:testing
contenitore finestra mobile ha gcc-10
a gcc-10.0.1
. Inoltre, non eseguire questi comandi se non ti fidi di me .
Spiacente, questo non è un esempio riproducibile minimo.
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
Poi nella console R, dopo la digitazione run
di ottenere gdb
per eseguire il programma:
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)
L'ispezione in gdb mostra abbastanza rapidamente (se ho capito bene) che
CSR_strmlen_x
sta provando ad accedere alla stringa che non è stata inizializzata.
AGGIORNAMENTO 2 : questa è una funzione altamente ricorsiva e, soprattutto, il bit di inizializzazione della stringa viene chiamato molte, molte volte. Questo è principalmente b / c. Ero pigro, abbiamo solo bisogno delle stringhe inizializzate per la volta in cui incontriamo qualcosa che vogliamo segnalare nella ricorsione, ma è stato più facile inizializzare ogni volta che è possibile incontrare qualcosa. Ne parlo perché ciò che vedrai in seguito mostra più inizializzazioni, ma solo una di esse (presumibilmente quella con indirizzo <0x1400000001>) viene utilizzata.
Non posso garantire che le cose che sto mostrando qui siano direttamente correlate all'elemento che ha causato il segfault (anche se è lo stesso indirizzo di accesso illegale), ma come ha chiesto @ nate-eldredge dimostra che l'elemento array non è inizializzato o appena prima del ritorno o subito dopo il ritorno nella funzione chiamante. Nota che la funzione di chiamata sta inizializzando 8 di questi e li mostro tutti, con tutti pieni di immondizia o memoria inaccessibile.
AGGIORNAMENTO 3 , smontaggio della funzione in questione:
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.
AGGIORNAMENTO 4 :
Quindi, cercando di analizzare lo standard qui ci sono le parti che sembrano rilevanti ( bozza C11 ):
6.3.2.3 Conversioni Par7> Altri operandi> Puntatori
Un puntatore a un tipo di oggetto può essere convertito in un puntatore a un diverso tipo di oggetto. Se il puntatore risultante non è allineato correttamente 68) per il tipo di riferimento, il comportamento non è definito.
Altrimenti, quando viene riconvertito di nuovo, il risultato deve essere paragonato al puntatore originale. Quando un puntatore a un oggetto viene convertito in un puntatore a un tipo di carattere, il risultato punta al byte indirizzato più basso dell'oggetto. Incrementi successivi del risultato, fino alla dimensione dell'oggetto, restituiscono puntatori ai byte rimanenti dell'oggetto.
6.5 Espressioni Par6
Il tipo effettivo di un oggetto per un accesso al suo valore memorizzato è il tipo dichiarato dell'oggetto, se presente. 87) Se un valore è memorizzato in un oggetto senza un tipo dichiarato attraverso un valore che ha un tipo che non è un tipo di carattere, allora il tipo del valore diventa il tipo effettivo dell'oggetto per quell'accesso e per gli accessi successivi che non lo fanno modifica il valore memorizzato. Se un valore viene copiato in un oggetto senza tipo dichiarato utilizzando memcpy o memmove, oppure viene copiato come una matrice di tipo di carattere, il tipo effettivo dell'oggetto modificato per quell'accesso e per gli accessi successivi che non modificano il valore è il tipo effettivo dell'oggetto da cui viene copiato il valore, se ne ha uno. Per tutti gli altri accessi a un oggetto che non ha un tipo dichiarato, il tipo effettivo dell'oggetto è semplicemente il tipo del valore utilizzato per l'accesso.
87) Gli oggetti allocati non hanno un tipo dichiarato.
IIUC R_alloc
restituisce un offset in un malloc
blocco ed che è garantito per essere double
allineato e la dimensione del blocco dopo l'offset è della dimensione richiesta (c'è anche allocazione prima dell'offset per dati specifici R). R_alloc
lancia quel puntatore (char *)
su al ritorno.
Sezione 6.2.5 Par 29
Un puntatore a vuoto deve avere gli stessi requisiti di rappresentazione e allineamento di un puntatore a un tipo di carattere. 48) Allo stesso modo, i puntatori a versioni qualificate o non qualificate di tipi compatibili devono avere gli stessi requisiti di rappresentazione e allineamento. Tutti i puntatori ai tipi di struttura devono avere gli stessi requisiti di rappresentazione e allineamento reciproci.
Tutti i puntatori ai tipi di unione devono avere gli stessi requisiti di rappresentazione e allineamento reciproci.
I puntatori ad altri tipi non devono avere gli stessi requisiti di rappresentazione o allineamento.48) Gli stessi requisiti di rappresentazione e allineamento hanno lo scopo di implicare l'interscambiabilità degli argomenti per le funzioni, restituire valori dalle funzioni e membri dei sindacati.
Quindi la domanda è "ci è permesso di rifondere il (char *)
to (const char **)
e scrivere come (const char **)
". La mia lettura di quanto sopra è che fintanto che i puntatori sui sistemi in cui viene eseguito il codice hanno un allineamento compatibile con l' double
allineamento, allora va bene.
Stiamo violando un "aliasing rigoroso"? vale a dire:
6.5 Par 7
Un oggetto deve avere il suo valore memorizzato accessibile solo da un'espressione lvalue che ha uno dei seguenti tipi: 88)
- un tipo compatibile con il tipo effettivo dell'oggetto ...
88) L'intento di questo elenco è quello di specificare le circostanze in cui un oggetto può essere o meno aliasato.
Quindi, quale dovrebbe essere il compilatore che dovrebbe pensare al tipo effettivo dell'oggetto indicato da res.target
(o res.current
)? Presumibilmente il tipo dichiarato (const char **)
, o è davvero ambiguo? Mi sembra che non sia in questo caso solo perché non esiste un altro "valore" nell'ambito che acceda allo stesso oggetto.
Devo ammettere che sto lottando intensamente per estrarre senso da queste sezioni dello standard.
-mtune=native
ottimizza per la CPU particolare che la tua macchina ha. Questo sarà diverso per i diversi tester e potrebbe essere parte del problema. Se esegui la compilation con -v
te dovresti essere in grado di vedere quale famiglia di cpu è sul tuo computer (ad esempio -mtune=skylake
sul mio computer).
disassemble
istruzioni all'interno di gdb.