У меня есть R пакет с C скомпилированным кодом, который был относительно стабильным в течение долгого времени и часто тестируется на широком спектре платформ и компиляторов (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"
В этот момент скомпилированный код сразу начал сегрегировать по следующим строкам:
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
Мне удалось последовательно воспроизвести segfault с помощью контейнера rocker/r-base
docker с gcc-10.0.1
с уровнем оптимизации -O2
. Запуск более низкой оптимизации избавляет от проблемы. Запуск любой другой установки, в том числе и под valgrind (как -O0, так и -O2), UBSAN (gcc / clang), не показывает никаких проблем. Я также вполне уверен, что это работает под gcc-10.0.0
, но у меня нет данных.
Я запустил версию gcc-10.0.1 -O2
с gdb
и заметил кое-что, что мне кажется странным:
При переходе по выделенному разделу кажется, что инициализация вторых элементов массивов пропускается (R_alloc
является оберткой вокруг malloc
, что само по себе мусор собирается при возврате управления в R, ошибка происходит перед возвратом в R). Позже, программа падает, когда происходит доступ к неинициализированному элементу (в версии g cc .10.0.1 -O2).
Я исправил это, явно инициализировав соответствующий элемент везде в коде, который в конечном итоге это привело к использованию элемента, но на самом деле его следовало инициализировать пустой строкой, или, по крайней мере, это то, что я бы предположил.
Я что-то упускаю из виду или совершаю глупости? Оба вполне вероятны, так как C мой второй язык на далеко . Просто странно, что это только что появилось, и я не могу понять, что пытается сделать компилятор.
UPDATE : инструкции по воспроизведению этого, хотя это будет воспроизводить только до тех пор, пока debian:testing
docker контейнер имеет 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
пытается получить доступ к строке, которая не был инициализирован.
UPDATE 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) У выделенных объектов нет объявленного типа .
IIU C R_alloc
возвращает смещение в malloc
ed блок, который гарантированно выровнен double
, а размер блока после смещения соответствует запрошенному размер (есть также распределение перед смещением для данных R, специфицированных c). R_alloc
возвращает этот указатель на (char *)
при возврате.
Раздел 6.2.5 Par 29
Указатель на void должен иметь те же требования к представлению и выравниванию, что и указатель на тип персонажа. 48) Аналогично, указатели на квалифицированные или неквалифицированные версии совместимых типов должны иметь одинаковые требования к представлению и выравниванию. Все указатели на типы структуры должны иметь те же требования к представлению и выравниванию, что и друг друга.
Все указатели на типы объединения должны иметь те же требования к представлению и выравниванию, что и другие.
Указатели на другие типы не обязательно должны иметь одинаковое представление. или требования к выравниванию.
48) Те же требования к представлению и выравниванию подразумевают взаимозаменяемость аргументов функций, возвращаемых значений из функций и членов объединений.
Таким образом, вопрос msgstr "нам разрешено переписать (char *)
в (const char **)
и записать в него как (const char **)
". Мое прочтение вышеизложенного состоит в том, что если указатели в системах, в которых выполняется код, имеют выравнивание, совместимое с выравниванием double
, то это нормально.
Нарушаем ли мы "строгий псевдоним"? то есть:
6.5 Par 7
Доступ к сохраненному значению объекта должен осуществляться только через выражение lvalue одного из следующих типов: 88)
- тип, совместимый с действующим типом объекта ...
88) Цель этого списка - указать те обстоятельства, при которых объект может или не может иметь псевдоним.
* 1136 Итак, что должен думать компилятор о
эффективном типе объекта, на который указывает
res.target
(или
res.current
)? Предположительно заявленный тип
(const char **)
, или это на самом деле неоднозначно? Мне кажется, что это не в этом случае только потому, что в области видимости нет другого «lvalue», который обращается к тому же объекту.
Я признаю, что изо всех сил пытаюсь извлечь смысл из этих разделов стандарта.