Почему%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 по-другому.

2 ответа

Решение

Из 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)

См. Этот раздел вопросов и ответов и вики-тег x86 для получения дополнительной информации или для получения последних ссылок ABI, если вышеуказанная ссылка устарела.

В x86_64 ABI, если функция имеет переменные аргументы, то 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).

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

Причина в эффективной реализации вариативных функций. Когда вариативная функция вызываетva_start, компилятору часто непонятно, va_argкогда-либо будет вызываться для аргумента с плавающей запятой. Следовательно, компилятор всегда должен сохранять все векторные регистры, которые могут содержать параметры, так что потенциальное будущееva_argcall может получить к нему доступ, даже если за это время регистр был затерт. Это довольно дорого, потому что на x86-64 имеется восемь таких регистров.

Следовательно, вызывающий передает число векторных регистров в качестве подсказки по оптимизации в вариативную функцию. Если в вызове нет векторных регистров, ни один из них не нужно сохранять. Например, началоsprintf функция в glibc выглядит так:

00000000000586e0 <_IO_sprintf@@GLIBC_2.2.5>:
   586e0:       sub    $0xd8,%rsp
   586e7:       mov    %rdx,0x30(%rsp)
   586ec:       mov    %rcx,0x38(%rsp)
   586f1:       mov    %r8,0x40(%rsp)
   586f6:       mov    %r9,0x48(%rsp)
   586fb:       test   %al,%al
   586fd:       je     58736 <_IO_sprintf@@GLIBC_2.2.5+0x56>
   586ff:       movaps %xmm0,0x50(%rsp)
   58704:       movaps %xmm1,0x60(%rsp)
   58709:       movaps %xmm2,0x70(%rsp)
   5870e:       movaps %xmm3,0x80(%rsp)
   58716:       movaps %xmm4,0x90(%rsp)
   5871e:       movaps %xmm5,0xa0(%rsp)
   58726:       movaps %xmm6,0xb0(%rsp)
   5872e:       movaps %xmm7,0xc0(%rsp)
   58736:       mov    %fs:0x28,%rax

На практике все реализации используют %alтолько как флаг, перепрыгивая через инструкции сохранения вектора, если он равен нулю. Вычисляемый goto во избежание сохранения ненужных регистров, похоже, не улучшает производительность.

Кроме того, если компиляторы могут обнаружить, что va_arg никогда не вызывается для аргумента с плавающей запятой, они полностью оптимизируют операцию сохранения векторного регистра, поэтому установка %alв таком случае является лишним. Но вызывающий не может знать эту деталь реализации, поэтому ему все равно придется установить%al.

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