У мене є пакет 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'
Мені вдалося послідовно відтворити сегментарій за допомогою 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; segfault відбувається перед поверненням до R). Пізніше програма виходить з ладу при доступі до неініціалізованого елемента (у версії gcc.10.0.1 -O2).
Я вирішив це, явно ініціалізувавши відповідний елемент скрізь у коді, що врешті-решт призвело до використання елемента, але воно справді мало бути ініціалізовано до порожнього рядка, або, принаймні, так я б припустив.
Я пропускаю щось очевидне чи роблю щось дурне? Обидва досить імовірно , як C мій друга мова по далеко . Це просто дивно, що це зараз закінчилося, і я не можу зрозуміти, що намагається зробити компілятор.
UPDATE : Інструкція відтворити це, хоча це буде відтворювати тільки до тих пір , як debian:testing
докер контейнер має 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
намагається отримати доступ до рядка, який не був ініціалізований.
ОНОВЛЕННЯ 2 : це дуже рекурсивна функція, і поверх цього біт ініціалізації рядків викликається багато, багато разів. Це здебільшого б / с, я був ледачий, нам потрібні лише рядки, ініціалізовані на той час, коли ми насправді зустрічаємо щось, про що хочемо повідомити в ході рекурсії, але ініціалізувати його було простіше кожного разу, коли можна щось зустріти. Я згадую про це, оскільки те, що ви побачите далі, показує декілька ініціалізацій, але використовується лише одна (імовірно, одна з адресою <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 ):
6.3.2.3 Перетворення Par7> Інші операнди> Покажчики
Вказівник на тип об'єкта може бути перетворений у вказівник на інший тип об'єкта. Якщо отриманий вказівник неправильно вирівняний 68) для посиланого типу, поведінка не визначена.
Інакше при повторному перетворенні результат порівнюється рівним вихідному вказівнику. Коли вказівник на об’єкт перетворюється на вказівник на тип символу, результат вказує на найнижчий адресований байт об'єкта. Послідовні прирости результату, аж до розміру об'єкта, дають покажчики на інші байти об'єкта.
6.5 Пар6 вирази
Ефективним типом об'єкта для доступу до його збереженого значення є оголошений тип об'єкта, якщо такий є. 87) Якщо значення зберігається в об'єкті, що не має оголошеного типу, через lvalue, що має тип, який не є символьним типом, то тип lvalue стає ефективним типом об'єкта для цього доступу та для подальших доступу, які не виконують змінити збережене значення. Якщо значення копіюється в об'єкт, що не має оголошеного типу, використовуючи memcpy або memmove, або скопіюється як масив типу символів, то ефективним типом модифікованого об'єкта для цього доступу та для подальших доступу, які не змінюють значення, є ефективний тип об'єкта, з якого скопійовано значення, якщо воно має. Для всіх інших доступу до об'єкта, що не має оголошеного типу, ефективним типом об'єкта є просто тип значення, що використовується для доступу.
87) Виділені об'єкти не мають заявленого типу.
IIUC R_alloc
повертає зміщення в malloc
блок ed, який гарантовано буде double
вирівняний, а розмір блоку після зміщення має шуканий розмір (також існує розподіл перед зміщенням для R-конкретних даних). R_alloc
кидає цей покажчик (char *)
на повернення.
Розділ 6.2.5 Пар. 29
Вказівник на недійсність повинен мати ті ж вимоги щодо представлення та вирівнювання, що і вказівник на тип символів. 48) Аналогічно, покажчики на кваліфіковані або некваліфіковані версії сумісних типів повинні мати однакові вимоги щодо представлення та вирівнювання. Усі вказівники на типи структур мають однакові вимоги до представлення та вирівнювання, як один до одного.
Усі вказівники на типи об'єднання мають однакові вимоги щодо представлення та вирівнювання, як один до одного.
Покажчики на інші типи не повинні мати однакових вимог щодо представлення чи вирівнювання.48) Одні й ті ж вимоги щодо представлення та вирівнювання мають на увазі взаємозамінність аргументів функцій, повернення значень функцій та членів союзів.
Так що питання «ми дозволили перероблено (char *)
в (const char **)
і записи на нього як (const char **)
». Моє читання вище сказаного полягає в тому, що доки покажчики в системах коду працюють, мають вирівнювання, сумісне з double
вирівнюванням, тоді все добре.
Ми порушуємо "суворий псевдонім"? тобто:
6.5 ч. 7
Об'єкт має збережене значення, доступ до якого має лише вираз lvalue, який має один із таких типів: 88)
- тип, сумісний з ефективним типом об'єкта ...
88) Метою цього списку є конкретизація тих обставин, за яких об'єкт може бути, а може і не бути відчуженим.
Отже, що повинен вважати компілятор ефективним типом об'єкта, на який вказує res.target
(або res.current
)? Імовірно, заявлений тип (const char **)
, чи це насправді неоднозначно? Мені здається, що це не в цьому випадку тільки тому, що немає іншого "значення" в області, яка отримує доступ до того ж об'єкта.
Я визнаю, що з усіх сил намагаюся витягнути сенс із цих розділів стандарту.
-mtune=native
оптимізується для конкретного процесора, який має ваша машина. Це буде різним для різних тестувальників і може бути частиною проблеми. Якщо ви запускаєте компіляцію з -v
вами, ви повинні мати можливість бачити, яка сім'я процесорів є у вашій машині (наприклад, -mtune=skylake
на моєму комп’ютері).
disassemble
інструкцію всередині gdb.