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

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

Ответы

Ответ 1

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

table from docs

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

Ответ 2

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

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

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

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

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

static foo(int x);

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

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

Ответ 3

Экспериментальный подход: разбирать код 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 до %rsp к sub, т.е. 5 щелчков после mov %rsp, %rbp
  • есть 6 толчков и 6 соответствующих попсов
  • другие инструкции не касаются %rsp

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