Я столкнулся с ошибкой в своем коде, из-за которой адрес локальной переменной стека является недействительным. Я пытался отладить это с помощью lldb.
В приглашении lldb ниже вы можете видеть, что &dominance_frontier
указывает адрес.
0x0000000000000001
, что затем вызывает сигнал SIGSEGV на hash_table_init
линии вызова 121.
Однако &dominator_tree_adj
указывает действительный адрес. Я совершенно сбит с толку
почему это может быть так.
Process 70998 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x0000000100006fd0 ir`ComputeDominanceFrontier(function=0x00006000030f8000) at dominators.c:121:9
111 struct Array postorder_traversal = postorder (function->entry_basic_block);
112 struct DFAConfiguration config = DominatorDFAConfiguration (function);
113 struct DFAResult result = run_DFA (&config, function);
114
115 HashTable dominator_tree_adj = ComputeDominatorTree (function, &result);
116 printf("%p\n", &dominator_tree_adj);
117 HashTable dominance_frontier;
118 printf("%ld\n", sizeof(dominance_frontier));
119
120
-> 121 hash_table_init (&dominance_frontier);
122 // Compute the transpose graph from the dominator tree adjacency list
123 // Each node is guaranteed to have only one direct predecessor, since
124 // each node can only have one immediate dominator. We will need this
125 // in the DF algorithm below
126 HashTable dominator_tree_transpose;
127 hash_table_init (&dominator_tree_transpose);
128
129 struct HashTableEntry *entry;
130 size_t entry_iter = 0;
131
Target 0: (ir) stopped.
(lldb) p &dominance_frontier
(HashTable *) 0x0000000000000001
(lldb) p &dominator_tree_adj
(HashTable *) 0x000000016fdfef38
Я компилировал свой код с помощью make DEBUG=yes
OPT = -O3
FLAGS = -Wall -Wextra
CC = cc
OBJECTS = ir_parser.o \
main.o \
threeaddr_parser.o \
instruction.o \
function.o \
basicblock.o \
constant.o \
utils.o \
value.o \
array.o \
mem.o \
map.o \
dfa.o \
dominators.o
ifdef DEBUG
OPT = -g
else
OPT = -O3
endif
all: $(OBJECTS)
$(CC) $(OPT) $(FLAGS) $^ -o ir
%.o: %.c *.h
$(CC) $(OPT) $(FLAGS) -c $< -o $@
clean:
rm *.o
Вот место сегфолта:
Process 10619 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x9)
frame #0: 0x0000000100005060 ir`_hash_table_init(table=0x0000000000000001, size=10) at map.c:21:21
11 // Open addressing, linear probe hash table.
12
13 unsigned long uint64_t_hash_function (uint64_t key)
14 {
15 // Simple hash function for demonstration
16 return key;
17 }
18
19 void _hash_table_init (HashTable *table, size_t size)
20 {
-> 21 table->size = size;
22 table->count = 0;
23 table->buckets = ir_calloc (table->size, sizeof (HashTableEntry));
24 }
25 void hash_table_init (HashTable *table)
26 {
27 _hash_table_init (table, MAP_INIT_SIZE_CNT);
28 }
29
30 // Create a new hash table
31 HashTable *hash_table_create (size_t size)
Target 0: (ir) stopped.
Входной файл:
fn test4(%1, %2) {
alloca %9, 5
add %3, %1, %2
store %9, %3
cmp %4, %1, %2
jumpif 1, %4
sub %5, %3, 1
jumpif 2, %5
1:
add %6, %1, 20
jump 3
2:
add %7, %2, 30
3:
sub %8, %3, %3
}
./ir -f path/to/input/file
Я использую Apple clang версии 15.0.0 (clang-1500.3.9.4).
Полный код здесь: https://github.com/CoconutJJ/compiler-optimization/blob/d3244e8c9f96e8180c924533abf8f5daac15238c/ir/dominators.c#L115
Запустите свой код через valgrind. Если вы неправильно управляете памятью, она подскажет вам, где именно.
Если вы поместите точку останова внутри hash_table_init
, вы увидите то же значение? Где на самом деле происходит ошибка segfault (вывод lldb, похоже, остановлен из-за одношагового режима, а не на реальном сайте SIGSEGV)? Наблюдаете ли вы такое же поведение с gdb? Какая это версия компилятора и архитектура?
@NateEldredge Я отредактировал вопрос, включив в него версию компилятора. У меня Apple Silicon M1. Я вижу то же значение в hash_table_init. Запуск этого докер-контейнера Ubuntu и использование вместо него gdb отображает segfault в строке возврата функции вместо этого в ComputeDominanceFrontier
.
Вы смотрели дизассемблирование рассматриваемой функции? Если она действительно намеренно передает 1 вместо допустимого адреса стека, что было бы ошибкой компилятора, то ассемблер этой функции должен быть явно неправильным.
Нам также понадобится входной файл.
@dbush Запуск этого с помощью valgrind внутри док-контейнера Ubuntu с valgrind --leak-check=yes
дает «Недопустимая запись размера 8». Но это не кажется очень полезным, поскольку речь идет просто о недействительном адресе, который я уже знаю.
@NateEldredge Я отредактировал вопрос, включив в него входной файл и команду запуска.
@NateEldredge Я попробую посмотреть сборку, хотя я не очень хорошо разбираюсь в ассемблерном коде ARM.
@Бармар Я не уверен, насколько меньше тебе нужен этот пример. Я мог бы предоставить простую программу на C, которая также выдает ошибки, но тогда это не помогло бы моей проблеме, поскольку я уже знал бы, что не так. Все файлы, которые я связал по ссылке на github, необходимы для компиляции моего кода. Пожалуйста, дайте мне знать, если у вас есть предложения о том, как я мог бы улучшить этот вопрос.
Идея состоит в том, что вы, насколько это возможно, создаете небольшую автономную программу, которая вызывает только эту функцию, а все остальное удаляется или заглушается.
Ах, я вижу это. dominators.h
объявляет ComputeDominanceFrontier
возвращающим void
, и это объявление видит вызывающий объект, но на самом деле он возвращает HashTable
. Я напишу ответ, объясняющий, как это вызывает то, что вы видите.
Или я думаю нет, так как он был закрыт. Но: когда функция возвращает достаточно большое struct
, вызывающая сторона должна выделить место для возвращаемого значения и передать на него указатель (в x8
для aarch64). Поскольку компилятор видит, что dominance_frontier
будет использоваться в качестве возвращаемого значения, он оптимизирует пространство своего стека (даже в -O0
) и вместо этого использует переданный указатель из x8
в качестве своего адреса. Здесь, поскольку main
думал, что функция вернулась void
, регистр x8
вместо этого просто содержит мусор.
Если бы вы включили dominators.h
в dominators.c
, как обычно, компилятор бы это уловил.
@NateEldredge ОГРОМНОЕ СПАСИБО! В этом была проблема! Не могу поверить, что я этого не видел. Я проголосовал за возобновление вопроса. Я приму ваш ответ.
HashTable ComputeDominanceFrontier (struct Function *function)
^^^^^^^^^
void ComputeDominanceFrontier (struct Function *function);
^^^^
Упс.
Функция ComputeDominanceFrontier
вызывается из main.c
, которая включает dominators.h
, поэтому вызывающая сторона думает, что она возвращает void
. Но согласно определению в dominators.c
, на самом деле оно возвращает HashTable
. Это несоответствие является причиной сбоя, как описано ниже.
Компилятор этого не улавливает, потому что вы не включили dominators.h
в dominators.c
, поэтому при компиляции dominators.c
он понятия не имел, что какой-то другой исходный файл видел конфликтующее объявление. По этой причине, когда заголовок объявляет глобальную функцию, вы всегда должны убедиться, что заголовок включен в исходный файл, определяющий функцию. Вы можете помочь обеспечить это с помощью -Wmissing-prototypes
, который выдает предупреждение всякий раз, когда глобальная функция определена без предварительного объявления. См. Предупреждение компилятора для функции, определенной без прототипа в области видимости?
Происходит следующее: на Arm64, как и на многих других платформах, когда функция возвращает тип struct
, который не может быть возвращен в регистре, он возвращается по «скрытой ссылке»: вызывающая сторона выделяет место для возвращаемого значения и передает дополнительный скрытый аргумент с адресом этого пространства. В Arm64 дополнительный аргумент передается в регистр x8
. Затем вызываемая функция отвечает за копирование возвращаемого значения в это пространство.
В этом случае, поскольку main
не знал, что ComputeDominanceFrontier
возвращает тип struct
, он не выделил такое пространство и оставил x8
, содержащий мусор (в вашем случае его значение оказалось 1
).
При наивной компиляции вы ожидаете увидеть сбой в конце функции, когда dominance_frontier
копируется из стека в фиктивный адрес возвращаемого значения. И похоже, что именно это вы видели в своем тесте Ubuntu (в котором, позвольте угадаю, вместо clang использовался gcc?).
Однако компилятор может оптимизировать это: вместо использования пространства стека для dominance_frontier
используйте пространство возвращаемого значения, выделенное вызывающей стороной. Тогда нам не нужно копировать эти данные перед возвратом, поскольку они были заполнены «на месте». Похоже, что clang выполняет эту оптимизацию, даже когда оптимизация «выключена». Таким образом, dominance_frontier
на самом деле не живет в фрейме стека ComputeDominanceFrontier
, и &dominance_frontier
является не адресом стека, а скорее адресом возвращаемого значения, переданным вызывающей стороной, что в данном случае является мусором.
Еще раз спасибо! Я добавлю -Wmissing-prototypes в свой make-файл.
@thebusybee: У ОП уже было -Wall -Wextra
. Они не включают -Wmissing-prototypes
, потому что нет ничего плохого в том, чтобы не объявлять функцию до ее определения, а в небольшой программе (например, файле с одним исходным кодом) это было бы бессмысленно и раздражающе.
Из публикуемого вами кода (никакого кода, кроме снимков экрана отладчика) можно сделать вывод, что вы используете:
I'm running into a bug in my code where the address of a local stack variable is invalid. I've been trying to debug this with lldb.
In the lldb prompt below, you can see that &dominance_frontier gives the address 0x0000000000000001 which then causes a SIGSEGV on hash_table_init call line 121. However &dominator_tree_adj, gives a valid address. I'm completely baffled as to why this might be the case.
Process 70998 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x0000000100006fd0 ir`ComputeDominanceFrontier(function=0x00006000030f8000) at dominators.c:121:9
111 struct Array postorder_traversal = postorder (function->entry_basic_block);
112 struct DFAConfiguration config = DominatorDFAConfiguration (function);
113 struct DFAResult result = run_DFA (&config, function);
114
115 HashTable dominator_tree_adj = ComputeDominatorTree (function, &result);
116 printf("%p\n", &dominator_tree_adj);
117 HashTable dominance_frontier;
118 printf("%ld\n", sizeof(dominance_frontier));
119
120
-> 121 hash_table_init (&dominance_frontier);
122 // Compute the transpose graph from the dominator tree adjacency list
123 // Each node is guaranteed to have only one direct predecessor, since
124 // each node can only have one immediate dominator. We will need this
125 // in the DF algorithm below
126 HashTable dominator_tree_transpose;
127 hash_table_init (&dominator_tree_transpose);
128
129 struct HashTableEntry *entry;
130
в строке 117 у вас есть
HashTable dominance_frontier;
это переменная, представляющая структуру HashTable, которую вы никогда не инициализируете. Всем остальным процедурам требуется указатель на переменную HashTable
, которая не была инициализирована. Я не знаю, почему вы не инициализируете структуру, но передача адреса будет мало или вообще не поможет, потому что все эти функции, похоже, требуют от вас вызова hash_table_create
для первоначального создания правильного содержимого структуры.
Вероятно, ваш код должен был быть
HashTable *dominator_tree_transpose = hash_table_create(number_of_hash_entries);
hash_table_init(dominator_tree_transpose);
но поскольку вы не предоставили исходный код, я не могу дальше гадать. Я не верю, что вы можете вызвать hash_table_init()
, передав ему адрес ОБЪЕКТА. Но я могу ошибаться. В следующий раз опубликуйте исходный код... не сеанс отладки. Включите полное определение типа HashTable
, функций hash_table_create()
и hash_table_init()
, поскольку использование обеих функций не может быть выведено из опубликованного кода.
Примечание. Я публикую это как ответ, потому что это, по-видимому, является причиной неудачи. Но из приведенных фрагментов это недостаточно ясно.
В идеале это было бы в рамках вопроса, но ОП связал полный код, и hash_table_init
все в порядке; он выполняет инициализацию. github.com/CoconutJJ/compiler-optimization/blob/…
Ну, я видел это позже, но этот пример не соответствует требованиям SO относительно минимальности и воспроизводимости. Необходимость создания полноценного приложения — это не способ показать вещи. Вместо того, чтобы голосовать за закрытие вопроса, я предпочел ответить исходя из написанного, а не копаться в целом многопроектном git-репозитории.
Пожалуйста, разместите здесь минимально воспроизводимый пример, а не ссылку на удаленный сайт.