Какие регистры сохраняются при вызове функции 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);
}
Скомпилировать и разобрать:
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
,
Как автор ассемблерного кода, вы тоже можете это сделать. Таким образом, вы можете реализовать "частное соглашение" между двумя или более процедурами, если это соглашение не мешает и не нарушает ожидания программного обеспечения, соответствующего стандартам.