Почему%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 ответа
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_arg
call может получить к нему доступ, даже если за это время регистр был затерт. Это довольно дорого, потому что на 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
.