Какие регистры сохраняются с помощью вызова функции 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.