Почему% eax обнуляется перед вызовом printf?

Я пытаюсь подобрать немного x86. Я компилирую на 64-битном mac с gcc -S -O0.

Код в C:

printf("%d", 1);

Вывод:

movl    $1, %esi
leaq    LC0(%rip), %rdi
movl    $0, %eax        ; WHY?
call    _printf

Я не понимаю, почему% eax очищается до 0 до вызова printf. Поскольку printf возвращает количество символов, напечатанных на %eax, моя лучшая догадка обнуляется, чтобы подготовить его для printf, но я предположил бы, что printf должен будет нести ответственность за его готовность. Кроме того, напротив, если я вызываю свою собственную функцию int testproc(int p1), gcc не видит необходимости готовить %eax. Поэтому мне интересно, почему gcc обрабатывает printf и testproc по-разному.

Ответ 1

Из x86_64 System V ABI:

Register    Usage
%rax        temporary register; with variable arguments
            passes information about the number of vector
            registers used; 1st return register
...

printf - это функция с переменными аргументами, а число используемых векторных регистров равно нулю.

Обратите внимание, что printf должен проверять только %al, поскольку вызывающему пользователю разрешено оставлять мусор в более высоких байтах %rax. (Тем не менее, xor %eax,%eax является наиболее эффективным способом для нуля %al)

Смотрите этот Q & A и x86 для более подробной информации или для современных ссылок ABI, если вышеуказанная ссылка устарела.

Ответ 2

В ABI x86_64, если функция имеет переменные аргументы, то ожидается, что AL (которая является частью EAX) будет содержать число векторных регистров, используемых для хранения аргументов этой функции.

В вашем примере:

printf("%d", 1);

имеет целочисленный аргумент, поэтому нет необходимости в векторном регистре, поэтому AL устанавливается в 0.

С другой стороны, если вы измените свой пример на:

printf("%f", 1.0f);

тогда литерал с плавающей запятой сохраняется в векторном регистре и, соответственно, AL устанавливается на 1:

movsd   LC1(%rip), %xmm0
leaq    LC0(%rip), %rdi
movl    $1, %eax
call    _printf

Как и ожидалось:

printf("%f %f", 1.0f, 2.0f);

заставит компилятор установить AL на 2, поскольку есть два аргумента с плавающей запятой:

movsd   LC0(%rip), %xmm0
movapd  %xmm0, %xmm1
movsd   LC2(%rip), %xmm0
leaq    LC1(%rip), %rdi
movl    $2, %eax
call    _printf

Что касается других вопросов:

puts также обнуляет %eax прямо перед вызовом, хотя он принимает только один указатель. Почему это?

Он не должен. Например:

#include <stdio.h>

void test(void) {
    puts("foo");
}

при компиляции с gcc -c -O0 -S, выходы:

pushq   %rbp
movq    %rsp, %rbp
leaq    LC0(%rip), %rdi
call    _puts
leave
ret

и %eax не обнуляется. Однако, если вы удалите #include <stdio.h>, тогда результирующая сборка обнуляет значение %eax перед вызовом puts():

pushq   %rbp
movq    %rsp, %rbp
leaq    LC0(%rip), %rdi
movl    $0, %eax
call    _puts
leave
ret

Причина связана с вашим вторым вопросом:

Это также происходит перед любым вызовом моей собственной функции void proc() (даже с установкой -O2), но при вызове функции void proc2 (int param) она не обнуляется.

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

Например:

#include <stdio.h>

void function() {
    puts("foo");
}

void test(void) {
    function();
}

где function() имеет пустой список параметров, результат:

pushq   %rbp
movq    %rsp, %rbp
movl    $0, %eax
call    _function
leave
ret

Однако, если вы следуете рекомендациям практики указания void, когда функция не принимает никаких параметров, например:

#include <stdio.h>

void function(void) {
    puts("foo");
}

void test(void) {
    function();
}

тогда компилятор знает, что function() не принимает аргументы - в частности, он не принимает переменные аргументы - и, следовательно, не очищает %eax до вызова этой функции:

pushq   %rbp
movq    %rsp, %rbp
call    _function
leave
ret