Какие регистры сохраняются при вызове функции linux x86-64

Мне кажется, я понимаю, как в Linux x86-64 ABI используются регистры и стек для передачи параметров в функцию (см. Предыдущее обсуждение ABI). Что меня смущает, так это то, что / что регистры должны сохраняться при вызове функции. То есть, какие регистры гарантированы, чтобы не быть забитыми?

2 ответа

Решение

Вот полная таблица регистров и их использование из документации [ PDF Link]:

стол из документов

r12, r13, r14, r15, rbx, rsp, rbp регистры, сохраненные вызываемым абонентом - у них есть "Да" в столбце "Сохранено через вызовы функций".

Экспериментальный подход: дизассемблировать код GCC

В основном для развлечения, но также для быстрой проверки того, что вы правильно поняли ABI.

Давайте попробуем затереть все регистры с помощью встроенной сборки, чтобы GCC сохранил и восстановил их:

main.c

#include <inttypes.h>

uint64_t inc(uint64_t i) {
    __asm__ __volatile__(
        ""
        : "+m" (i)
        :
        : "rax",
          "rbx",
          "rcx",
          "rdx",
          "rsi",
          "rdi",
          "rbp",
          "rsp",
          "r8",
          "r9",
          "r10",
          "r11",
          "r12",
          "r13",
          "r14",
          "r15",
          "ymm0",
          "ymm1",
          "ymm2",
          "ymm3",
          "ymm4",
          "ymm5",
          "ymm6",
          "ymm7",
          "ymm8",
          "ymm9",
          "ymm10",
          "ymm11",
          "ymm12",
          "ymm13",
          "ymm14",
          "ymm15"
    );
    return i + 1;
}

int main(int argc, char **argv) {
    (void)argv;
    return inc(argc);
}

GitHub вверх по течению.

Скомпилировать и разобрать:

 gcc -std=gnu99 -O3 -ggdb3 -Wall -Wextra -pedantic -o main.out main.c
 objdump -d main.out

Разборка содержит:

00000000000011a0 <inc>:
    11a0:       55                      push   %rbp
    11a1:       48 89 e5                mov    %rsp,%rbp
    11a4:       41 57                   push   %r15
    11a6:       41 56                   push   %r14
    11a8:       41 55                   push   %r13
    11aa:       41 54                   push   %r12
    11ac:       53                      push   %rbx
    11ad:       48 83 ec 08             sub    $0x8,%rsp
    11b1:       48 89 7d d0             mov    %rdi,-0x30(%rbp)
    11b5:       48 8b 45 d0             mov    -0x30(%rbp),%rax
    11b9:       48 8d 65 d8             lea    -0x28(%rbp),%rsp
    11bd:       5b                      pop    %rbx
    11be:       41 5c                   pop    %r12
    11c0:       48 83 c0 01             add    $0x1,%rax
    11c4:       41 5d                   pop    %r13
    11c6:       41 5e                   pop    %r14
    11c8:       41 5f                   pop    %r15
    11ca:       5d                      pop    %rbp
    11cb:       c3                      retq   
    11cc:       0f 1f 40 00             nopl   0x0(%rax)

Итак, мы ясно видим, что следующие элементы выталкиваются и выталкиваются:

rbx
r12
r13
r14
r15
rbp

Единственное, чего не хватает в спецификации: rsp, но мы, конечно, ожидаем восстановления стека. Внимательное прочтение сборки подтверждает, что в этом случае она сохраняется:

  • sub $0x8, %rsp: выделяет 8 байтов в стеке для сохранения %rdi в %rdi, -0x30(%rbp), что сделано для встроенной сборки +m ограничение
  • lea -0x28(%rbp), %rsp восстанавливает %rsp назад к до sub, т.е. через 5 всплывающих окон после mov %rsp, %rbp
  • есть 6 нажатий и 6 соответствующих хлопков
  • другие инструкции не касаются %rsp

Протестировано в Ubuntu 18.10, GCC 8.2.0.

ABI определяет, чего можно ожидать от стандартного программного обеспечения. Он написан в первую очередь для авторов компиляторов, компоновщиков и других программ обработки языка. Эти авторы хотят, чтобы их компилятор создавал код, который будет правильно работать с кодом, который компилируется тем же (или другим) компилятором. Все они должны согласиться с набором правил: как формальные аргументы функций передаются от вызывающего к вызываемому, как возвращаемые функцией значения возвращаются от вызывающего к вызывающему, какие регистры сохраняются / нуля / не определены через границу вызова и т. Д. на.

Например, одно правило гласит, что сгенерированный код сборки для функции должен сохранить значение сохраненного регистра перед изменением значения, и что код должен восстановить сохраненное значение, прежде чем вернуться к своему вызывающему. Для чистого регистра сгенерированный код не требуется для сохранения и восстановления значения регистра; он может сделать это, если захочет, но стандартное программное обеспечение не может зависеть от этого поведения (если оно не является стандартным программным обеспечением).

Если вы пишете ассемблерный код, вы несете ответственность за игру по тем же правилам (вы играете роль компилятора). То есть, если ваш код изменяет регистр, сохраненный вызываемым пользователем, вы несете ответственность за вставку инструкций, которые сохраняют и восстанавливают исходное значение регистра. Если ваш ассемблерный код вызывает внешнюю функцию, ваш код должен передавать аргументы стандартным образом, и это может зависеть от того факта, что, когда вызываемый объект возвращает, сохраненные значения регистра фактически сохраняются.

Правила определяют, как программное обеспечение, соответствующее стандартам, может обойтись. Тем не менее, совершенно законно писать (или генерировать) код, который не воспроизводится по этим правилам! Компиляторы делают это постоянно, потому что они знают, что правила не должны соблюдаться при определенных обстоятельствах.

Например, рассмотрим функцию C с именем foo, которая объявлена ​​следующим образом, и ее адрес никогда не берется:

static foo(int x);

Во время компиляции компилятор на 100% уверен, что эта функция может быть вызвана только другим кодом в файле (ах), которые он в данный момент компилирует. функция foo не может быть вызван ничем другим, когда-либо, учитывая определение того, что значит быть статичным. Потому что компилятор знает всех вызывающих foo во время компиляции компилятор может свободно использовать любую вызывающую последовательность, какую захочет (вплоть до того, чтобы вообще не делать вызов, то есть вставлять код для foo в вызывающих foo,

Как автор ассемблерного кода, вы тоже можете это сделать. Таким образом, вы можете реализовать "частное соглашение" между двумя или более процедурами, если это соглашение не мешает и не нарушает ожидания программного обеспечения, соответствующего стандартам.

Другие вопросы по тегам