Почему вызов функции C abort () из функции сборки x86_64 приводит к ошибке сегментации (SIGSEGV) вместо сигнала прерывания? - PullRequest
3 голосов

Рассмотрим программу:

main.c

#include <stdlib.h>

void my_asm_func(void);
__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "call abort;"
    "ret;"
);

int main(int argc, char **argv) {
    if (argv[1][0] == '0') {
        abort();
    } else if (argv[1][0] == '1') {
        __asm__("call abort");
    } else {
        my_asm_func();
    }
}

, которую я компилирую как:

gcc -ggdb3 -O0 -o main.out main.c

Тогда у меня есть:

$ ./main.out 0; echo $?
Aborted (core dumped)
134
$ ./main.out 1; echo $?
Aborted (core dumped)
134
$ ./main.out 2; echo $?
Segmentation fault (core dumped)
139

Почему я получаю ошибку сегментации только для последнего запуска, а не сигнал прерывания, как ожидалось?

man 7 сигнал:

   SIGABRT       6       Core    Abort signal from abort(3)
   SIGSEGV      11       Core    Invalid memory reference

подтверждает сигналы из-за 128+ SIGNUM rule.

В качестве проверки работоспособности я также попытался выполнить другие вызовы функций из сборки, как в:

#include <stdlib.h>

void my_asm_func(void);
__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "lea puts_message(%rip), %rdi;"
    "call puts;"
    "ret;"
    "puts_message: .asciz \"hello puts\""
);

int main(void) {
    my_asm_func();
}

, и это сработало, и выведите:

hello puts

Протестировано в Ubuntu 19.04 amd64, GCC 8.3.0, glibc 2.29.

Я также пробовал это в докере Ubunt Ubuntu 18.04, и результаты были такими же, за исключением того, что программа выдает при запуске:

./main.out: Symbol `abort' causes overflow in R_X86_64_PC32 relocation          
./main.out: Symbol `abort' causes overflow in R_X86_64_PC32 relocation

, который кажется хорошей подсказкой.

1 Ответ

4 голосов
/ 27 мая 2019

В этом коде, который определяет функцию в глобальной области видимости (с базовой сборкой):

void my_asm_func(void);

__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "call abort;"
    "ret;"
);

Вы нарушаете одно из правил ABI System V x86-64 (AMD64), которое требует выравнивания стека 16 байт (может быть больше в зависимости от параметров) в точке непосредственно перед тем, как будет CALL.

3.2.2 Рамка стека

В дополнение к регистрам у каждой функции есть кадр в стеке времени выполнения. Этот стек растет сверху вниз адреса. На рисунке 3.3 показана организация стека.

T конец области входного аргумента должен быть выровнен по 16 (32, если передано __m256 в стеке) граница байта . Другими словами, значение (% rsp + 8) всегда кратно 16 (32), когда управление передается точка входа в функцию. Указатель стека,% rsp, всегда указывает на конец последнего выделенного стекового кадра.

При входе в функцию стек будет смещен на 8, поскольку 8-байтовый адрес возврата теперь находится в стеке. Чтобы выровнять стек по 16-байтовой границе, вычтите 8 из RSP в начале функции и добавьте 8 обратно к RSP, когда закончите. Вы также можете просто нажать любой регистр, например RBP в начале, и нажать его после, чтобы получить тот же эффект.

Эта версия кода должна работать:

void my_asm_func(void);

__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "push %rbp;"
    "call abort;"
    "pop %rbp;"
    "ret;"
);

Относительно этого кода, который работал:

__asm__("call abort");

Компилятор, вероятно, сгенерировал функцию main так, чтобы стек был выровнен по 16-байтовой границе до этого вызова, так что это сработало. Вы не должны полагаться на это поведение. Существуют и другие потенциальные проблемы с этим кодом, но в этом случае их нельзя представить как сбой. Стек должен быть правильно выровнен перед вызовом; вы должны быть обеспокоены в целом красной зоной; и вы должны указать все энергозависимые регистры в соглашениях о вызовах как клобберы, включая RAX / RCX / RDX / R8 / R9 / R10 / R11 , регистры FPU и регистры SIMD. В этом случае abort никогда не возвращается, так что это не проблема, связанная с вашим кодом.

Красная зона определяется в ABI следующим образом:

128-байтовая область за пределами места, на которое указывает% rsp, считается зарезервировано и не должно изменяться обработчиками сигналов или прерываний.8 Следовательно, функции могут использовать эту область для временных данных, которые не нужны для всей функции звонки. В частности, листовые функции могут использовать эту область для всего кадра стека, вместо настройки указателя стека в прологе и эпилоге. Эта область известный как красная зона .

Обычно плохая идея вызывать функцию во встроенной сборке. Пример вызова printf можно найти в этом другом ответе Stackoverflow , который показывает сложности выполнения CALL, особенно в 64-битном коде с красной зоной. Дэвида Уолферда: «Не используйте встроенный ассм» - всегда хорошее чтение.


Этот код работал:

void my_asm_func(void);
__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "lea puts_message(%rip), %rdi;"
    "call puts;"
    "ret;"
    "puts_message: .asciz \"hello puts\""
);

но вам, вероятно, повезло, что puts не нуждался в правильном выравнивании, и вы случайно не потерпели неудачу. Вы должны выровнять стек перед вызовом puts, как описано ранее, с my_asm_func, который вызвал abort. Обеспечение соответствия ABI является ключом к тому, чтобы код работал должным образом.


Что касается ошибок перемещения, возможно, это связано с тем, что используемая версия Ubuntu по умолчанию использует независимый от позиции код (PIC) для генерации кода GCC. Вы можете решить эту проблему, вызвав библиотечные вызовы C через таблицу связей процедур 1070 *, добавив @plt к именам функций, которые вы CALL. Peter Cordes написал соответствующий Stackoverflow ответ на эту тему.

...