Система libc (), когда указатель стека не заполнен 16, вызывает ошибку сегментации - PullRequest
0 голосов
/ 28 января 2019

Я заметил действительно странное поведение, когда я играл с функцией libc system () в Linux x86-64, иногда вызов system() завершается с ошибкой сегментации, вот что я получил после отладки с помощью gdb.

Я заметил, что ошибка сегментации возникает в этой строке:

=> 0x7ffff7a332f6 <do_system+1094>: movaps XMMWORD PTR [rsp+0x40],xmm0

Согласно руководству , это является причиной SIGSEGV:

Когда операндом источника или назначения является операнд памяти, операнд должен быть выровнен по 16-байтовой границе или генерируется исключение общей защиты (#GP).

Глядя глубже, я заметил, что действительно мое значение rsp не было дополнено 16 байтами (то есть его шестнадцатеричное представление не заканчивалось 0).Изменение вручную rsp прямо перед вызовом system фактически заставляет все работать.

Итак, я написал следующую программу:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    register long long int sp asm ("rsp");
    printf("%llx\n", sp);

    if (sp & 0x8) /* == 0x8*/
    { 
        printf("running system...\n");
        system("touch hi");
    } 

    return 0;
}

Скомпилировано с gcc 7.3.0 Иконечно же, при наблюдении за выводом:

sha@sha-desktop:~/Desktop/tda$ ltrace -f ./o_sample2
[pid 26770] printf("%llx\n", 0x7ffe3eabe6c87ffe3eabe6c8
)                                           = 13
[pid 26770] puts("running system..."running system...
)                                                  = 18
[pid 26770] system("touch hi" <no return ...>
[pid 26771] --- SIGSEGV (Segmentation fault) ---
[pid 26771] +++ killed by SIGSEGV +++
[pid 26770] --- SIGCHLD (Child exited) ---
[pid 26770] <... system resumed> )           = 139
[pid 26770] +++ exited (status 0) +++

Итак, с этой программой я не могу выполнить system() что бы то ни было.

Небольшая вещь, и я не могу сказать, относится ли она кпроблема, почти все мои пробеги заканчиваются с плохим значением rsp и ребенком, который убит SEGSEGV.

Это заставляет меня задуматься о нескольких вещах:

  1. Почемуsystem портится с xmm регистрами?
  2. Это нормальное поведение?или, может быть, мне не хватает чего-то элементарного в отношении того, как правильно использовать функцию system()?

Заранее спасибо

1 Ответ

0 голосов
/ 28 января 2019

x86-64 System V ABI гарантирует 16-байтовое выравнивание стека перед call, поэтому libc system может использовать это преимущество для 16-байтовых выровненных загрузок / хранилищ .Если вы нарушаете ABI, это ваша проблема, если что-то пойдет не так.

При входе в функцию, после того, как call выдвинул адрес возврата, RSP + -8 выровнен на 16 байт, и еще один push настроит вас на вызов другой функции.

Конечно, GCC обычно не имеет проблем с этим, используя либо нечетное число push es, либо sub rsp, 16*n + 8, чтобы зарезервировать пространство стека.Использование локальной переменной register-asm с asm("rsp") не нарушает этого, если только вы читаете переменную, а не присваиваете ей.

Вы говорите, что используете GCC7.3. Я поместил ваш код в проводник компилятора Godbolt и скомпилировал его с помощью -O3, -O2, -O1 и -O0.Он следует ABI на всех уровнях оптимизации, создавая main, который начинается с sub rsp, 8 и не изменяет RSP внутри функции (за исключением call), до конца функции.

Так же как и все остальные версии и уровни оптимизации clang и gcc, которые я проверял.

Это код ggen7.3 -O3: обратите внимание, что он не делает что-либо с RSP, кроме чтения еговнутри тела функции, поэтому если main вызывается с действительным RSP (16-байтовым выравниванием - 8), все вызовы функций main также будут выполняться с 16-байтовым RSP.( И он никогда не найдет sp & 8 истину, поэтому он никогда не позвонит system, во-первых. )

# gcc7.3 -O3
main:
        sub     rsp, 8
        xor     eax, eax
        mov     edi, OFFSET FLAT:.LC0
        mov     rsi, rsp          # read RSP.
        call    printf
        test    spl, 8            # low 8 bits of RSP
        je      .L2
        mov     edi, OFFSET FLAT:.LC1
        call    puts
        mov     edi, OFFSET FLAT:.LC2
        call    system
.L2:
        xor     eax, eax
        add     rsp, 8
        ret

Если вы звоните main каким-то нестандартным образом вы нарушаете ABI .И вы не объясняете это в этом вопросе, так что это не MCVE .

Как я объяснил в Допускает ли стандарт C ++ неинициализированный bool для сбоя программы? , компиляторам разрешено выдавать код, который использует любые гарантии, которые дает ABI целевой платформы.Это включает использование movaps для 16-байтовых загрузок / хранилищ для копирования содержимого в стеке с использованием гарантии входного выравнивания.


Это пропущенная оптимизация, которую gcc не оптимизируетif() полностью, как clang.

Но Clang действительно рассматривает его как неинициализированную переменную;без использования его в операторе asm, поэтому локальный регистр asm("rsp"), по-моему, никак не влияет на clang.Clang оставляет RSI неизмененным перед первым printf вызовом, поэтому main clang фактически печатает argv, вообще не читая RSP.

Clang разрешено делать это: поддерживается только использование локальных переменных register-asm заставляет "r"(var) расширенных ограничений asm выбрать нужный регистр.(https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html).

Руководство не подразумевает, что простое использование такой переменной в других случаях может быть проблематичным, поэтому я думаю, что этот код должен быть безопасным в целом в соответствии с написанными правилами, а также с работойна практике.

В руководстве сказано, что использование закрытого для вызова регистра (например, "rcx" на x86) приведет к тому, что переменная будет перекрыта вызовами функций, поэтому, возможно, будет затронута переменная, использующая rspсгенерированным компилятором push / pop?

Это интересный тестовый пример: посмотрите его по ссылке Godbolt.

// gcc won't compile this: "error: unable to find a register to spill"
// clang simply copies the value back out of RDX before idiv
int sink;
int divide(int a, int b) {
    register long long int dx asm ("rdx") = b;
    asm("" : "+r"(dx));  // actually make the compiler put the value in RDX

    sink = a/b;   // IDIV uses EDX as an input

    return dx;
}

Без asm("" : "+r"(dx));, gcc компилирует это просто отлично,никогда не помещать b в RDX вообще.

...