Почему значение EDX перезаписывается при вызове printf?
Я написал простую программу ассамблеи:
section .data
str_out db "%d ",10,0
section .text
extern printf
extern exit
global main
main:
MOV EDX, ESP
MOV EAX, EDX
PUSH EAX
PUSH str_out
CALL printf
SUB ESP, 8 ; cleanup stack
MOV EAX, EDX
PUSH EAX
PUSH str_out
CALL printf
SUB ESP, 8 ; cleanup stack
CALL exit
Я ассемблер NASM и GCC, чтобы связать объектный файл с исполняемым файлом в linux.
по сути, эта программа сначала помещает значение указателя стека в регистр EDX, а затем печатает содержимое этого регистра дважды. Однако после второго вызова printf значение, напечатанное в stdout, не совпадает с первым.
такое поведение кажется странным. Когда я заменяю каждый использование EDX в этой программе с EBX, выведенные целые числа идентичны, как и ожидалось. Я могу только предположить, что EDX перезаписывается в какой-то момент во время вызова функции printf.
почему это произошло? И как я могу убедиться, что регистры, которые я использую в будущем, не конфликтуют с функциями c lib?
2 ответов
по словам x86 ABI, EBX
, ESI
, EDI
и EBP
являются callee-сохранить регистры и EAX
, ECX
и EDX
являются caller-сохранить регистры.
это означает, что функции могут свободно использовать и уничтожить прежние ценности EAX
, ECX
и EDX
.
По этой причине сохраните значения EAX
, ECX
, EDX
перед вызовом функции, если вы не хотите, чтобы их значения меняются. Это то, что означает" caller-save".
или лучше, используйте другие регистры для значений, которые вам все еще понадобятся после вызова функции. push / pop EBX
в начале / конце функции намного лучше, чем push / pop EDX
внутри цикла, который делает вызов функции. Когда это возможно, используйте регистры Call-clobbered для временных, которые не нужны после вызова. Значения, которые уже находятся в памяти, поэтому их не нужно записывать перед перечитыванием, также дешевле разливать.
С EBX
, ESI
, EDI
, и EBP
являются регистры callee-save, функции должны восстановить значения в оригинале для любого из тех, которые они изменяют, перед возвращением.
ESP
также сохраняется вызываемый, но вы не можете испортить это, если вы не скопируете обратный адрес где-нибудь. Несоответствующий вызов / ret ужасен для производительности, потому что современные процессоры используют предиктор обратного адреса.
ABI для целевой платформы (например, 32bit x86 Linux) определяет, какие регистры могут использоваться функциями без сохранения. (т. е., если вы хотите, чтобы они сохранились на звонок, вы должны сделать это сами).
ссылки на документы ABI для Windows и non-Window, 32 и 64bit, at https://stackoverflow.com/tags/x86/info
наличие некоторых регистров, которые не сохраняются между вызовами (доступны как регистры нуля), означает, что функции могут быть меньше. Простой функции часто могут избежать выполнения любых push/pop
сохранение/восстановление. Это сокращает количество инструкций, что приводит к более быстрому коду.
важно иметь некоторые из них: необходимость разливать все состояние в память по вызовам раздула бы код не-листовых функций и замедлила бы esp. в случаях, когда вызываемая функция не коснулась всех регистров.