Mam pakiet R z kompilowanym kodem C, który był stosunkowo stabilny od dłuższego czasu i jest często testowany na wielu różnych platformach i kompilatorach (windows / osx / debian / fedora gcc / clang).
Niedawno dodano nową platformę do ponownego przetestowania pakietu:
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"
W tym momencie skompilowany kod natychmiast zaczął segfaultować według następujących linii:
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
Byłem w stanie konsekwentnie odtwarzać segfault przy użyciu rocker/r-base
kontenera dokowanego gcc-10.0.1
z poziomem optymalizacji -O2
. Uruchomienie niższej optymalizacji pozbywa się problemu. Uruchamianie innych konfiguracji, w tym w ramach valgrind (zarówno -O0, jak i -O2), UBSAN (gcc / clang), nie wykazuje żadnych problemów. Jestem również dość pewien, że to się skończyło gcc-10.0.0
, ale nie mam danych.
Uruchomiłem gcc-10.0.1 -O2
wersję gdb
i zauważyłem coś, co wydaje mi się dziwne:
Podczas przechodzenia przez podświetloną sekcję wydaje się, że inicjalizacja drugich elementów tablic jest pomijana ( R_alloc
jest to opakowanie, malloc
które gromadzi samo śmieci, gdy wraca kontrola do R; segfault zdarza się przed powrotem do R). Później program ulega awarii, gdy uzyskiwany jest dostęp do niezainicjowanego elementu (w wersji gcc.10.0.1 -O2).
Naprawiłem to, jawnie inicjując dany element wszędzie w kodzie, co ostatecznie doprowadziło do jego użycia, ale tak naprawdę powinien był zostać zainicjowany na pusty ciąg, a przynajmniej tak bym się spodziewał.
Czy brakuje mi czegoś oczywistego lub robię coś głupiego? Oba są dość prawdopodobne, ponieważ C jest zdecydowanie moim drugim językiem . Dziwne, że to właśnie się pojawiło i nie mogę zrozumieć, co kompilator próbuje zrobić.
UPDATE : Instrukcje do odtworzenia tego, choć będzie to jedynie odtworzyć tak długo, jak debian:testing
pojemnik doker ma gcc-10
co gcc-10.0.1
. Ponadto, nie wystarczy uruchomić te polecenia, jeśli nie ufasz mi .
Niestety nie jest to minimalny odtwarzalny przykład.
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
Następnie w konsoli R, po wpisaniu run
aby gdb
uruchomić 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)
Sprawdzanie w gdb dość szybko pokazuje (o ile dobrze rozumiem), że
CSR_strmlen_x
próbuje uzyskać dostęp do łańcucha, który nie został zainicjowany.
AKTUALIZACJA 2 : jest to wysoce rekurencyjna funkcja, a ponadto bit inicjujący ciąg jest wywoływany wiele, wiele razy. Jest to głównie b / c byłem leniwy, potrzebujemy tylko zainicjowanych łańcuchów, gdy napotkamy coś, co chcemy zgłosić w rekurencji, ale łatwiej było zainicjalizować za każdym razem, gdy można coś napotkać. Wspominam o tym, ponieważ to, co zobaczysz później, pokazuje wiele inicjalizacji, ale używana jest tylko jedna z nich (prawdopodobnie ta o adresie <0x1400000001>).
Nie mogę zagwarantować, że rzeczy, które tu pokazuję, są bezpośrednio związane z elementem, który spowodował segfault (chociaż jest to ten sam nielegalny dostęp do adresu), ale jak zapytał @ nate-eldredge, pokazuje, że element tablicy nie jest zainicjowany albo tuż przed powrotem, albo tuż po powrocie w funkcji wywoływania. Zauważ, że funkcja wywołująca inicjuje 8 z nich, i pokazuję je wszystkie, wszystkie wypełnione śmieciami lub niedostępną pamięcią.
AKTUALIZACJA 3 , demontaż danej funkcji:
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.
AKTUALIZACJA 4 :
Tak więc próbując przeanalizować tutaj standard, jego części wydają się istotne ( projekt C11 ):
6.3.2.3 Konwersje Par7> Inne argumenty> Wskaźniki
Wskaźnik do typu obiektu można przekonwertować na wskaźnik do innego typu obiektu. Jeśli wynikowy wskaźnik nie jest poprawnie wyrównany 68) dla przywoływanego typu, zachowanie jest niezdefiniowane.
W przeciwnym razie, po ponownej konwersji wynik będzie porównywany z oryginalnym wskaźnikiem. Kiedy wskaźnik do obiektu jest konwertowany na wskaźnik do typu znaku, wynik wskazuje na najniższy adresowany bajt obiektu. Kolejne przyrosty wyniku, aż do wielkości obiektu, dają wskaźniki do pozostałych bajtów obiektu.
6.5 Wyrażenia Par6
Efektywnym typem obiektu dla dostępu do jego przechowywanej wartości jest deklarowany typ obiektu, jeśli taki istnieje. 87) Jeśli wartość jest przechowywana w obiekcie, który nie ma zadeklarowanego typu poprzez wartość o typie innym niż typ znaku, wówczas typ wartości staje się efektywnym typem obiektu dla tego dostępu i dla kolejnych wejść, które nie zmodyfikuj zapisaną wartość. Jeśli wartość jest kopiowana do obiektu bez zadeklarowanego typu przy użyciu memcpy lub memmove, lub jest kopiowana jako tablica typu znaków, to skutecznym typem zmodyfikowanego obiektu dla tego dostępu i dla kolejnych dostępów, które nie modyfikują wartości, jest efektywny typ obiektu, z którego kopiowana jest wartość, jeśli ją posiada. W przypadku wszystkich innych wejść do obiektu bez zadeklarowanego typu efektywnym typem obiektu jest po prostu typ wartości użytej do uzyskania dostępu.
87) Przydzielone obiekty nie mają zadeklarowanego typu.
IIUC R_alloc
zwraca przesunięcie do edytowanego malloc
bloku, który gwarantuje double
wyrównanie, a rozmiar bloku po przesunięciu ma żądany rozmiar (przed przesunięciem dla danych specyficznych istnieje również alokacja). R_alloc
rzutuje ten wskaźnik (char *)
na powrót.
Sekcja 6.2.5 Par 29
Wskaźnik do unieważnienia powinien mieć takie same wymagania dotyczące reprezentacji i wyrównania, jak wskaźnik do typu znaku. 48) Podobnie wskaźniki do kwalifikowanych lub niekwalifikowanych wersji kompatybilnych typów powinny mieć takie same wymagania dotyczące reprezentacji i wyrównania. Wszystkie wskaźniki do typów konstrukcji powinny mieć takie same wymagania dotyczące reprezentacji i wyrównania.
Wszystkie wskaźniki do typów unii mają takie same wymagania dotyczące reprezentacji i wyrównania.
Wskaźniki do innych typów nie muszą mieć takich samych wymagań dotyczących reprezentacji lub wyrównania.48) Te same wymagania dotyczące reprezentacji i wyrównania mają na celu sugerowanie wymienności jako funkcji dla funkcji, zwracania wartości z funkcji i członków związków.
Więc pytanie brzmi „czy wolno nam przekształcenie (char *)
się (const char **)
i napisz do niego jako (const char **)
”. Mój odczyt powyższego jest taki, że dopóki wskaźniki w systemach, w których działa kod, mają wyrównanie zgodne z double
wyrównaniem, to jest w porządku.
Czy naruszamy „ścisłe aliasing”? to znaczy:
6,5 Par 7
Dostęp do przechowywanej wartości obiektu może mieć wyłącznie wyrażenie wartości, które ma jeden z następujących typów: 88)
- typ zgodny z efektywnym typem obiektu ...
88) Celem tej listy jest określenie tych okoliczności, w których obiekt może być lub nie być aliasowany.
Więc co kompilator powinien uważać za efektywny typ obiektu wskazywanego przez res.target
(lub res.current
)? Prawdopodobnie zadeklarowany typ (const char **)
, czy jest to rzeczywiście niejednoznaczne? Wydaje mi się, że nie jest tak w tym przypadku tylko dlatego, że nie ma innej „wartości” w zakresie dostępu do tego samego obiektu.
Przyznaję, że ciężko walczę o wydobycie sensu z tych części standardu.
-mtune=native
optymalizuje pod kątem konkretnego procesora, który ma Twój komputer. Będzie to różne dla różnych testerów i może być częścią problemu. Jeśli uruchomisz kompilację -v
, powinieneś być w stanie zobaczyć, która rodzina procesorów znajduje się na twoim komputerze (np. -mtune=skylake
Na moim komputerze).
disassemble
instrukcji w gdb.