clo*_*0rk 6 x86 assembly callstack abi red-zone
在x86汇编语言中:
我假设我有一个正常的功能序言,请阅读
push ebp
mov ebp,esp
Run Code Online (Sandbox Code Playgroud)
我知道我可以通过访问内存目标操作数来读取或写入寄存器,假设我想要第一个参数。我会做
mov eax,[ebp +8]
Run Code Online (Sandbox Code Playgroud)
从堆栈中获取一个整数参数。
那为什么我不直接使用stackpointer呢?
add esp,8 ; point ESP at the data we want
pop eax
sub esp,12 ; restore ESP to its original position
Run Code Online (Sandbox Code Playgroud)
这会导致错误吗?在任何情况下都使用吗?
我当然知道第一个操作的大小较小,因为它只是一个操作码,mov而不是三个,但这不是问题的重点。
(编者注:mov eax, [ebp+8]。在x86机器代码的3字节的指令 add/ subESP,imm8指定为3个字节的每个,pop eax是1个字节。
mov eax, [esp+8]是一个4字节的指令:不像在16位寻址模式,ESP可以是一个基址寄存器。但是它确实需要一个SIB字节来对其进行编码。
这些都是现代CPU上的单联指令,不包括额外的堆栈同步联指令。)
为什么这样做是错误的做法?
您可以ESP直接使用作为指针。
但是,如果发生任何推动或弹出操作,ESP就会变成移动目标,这会使您的计算更加困难。
因此,我们将堆栈指针的副本放在EBP中,因此我们不必担心ESP更改。
但是,如果您不打算做任何更改堆栈指针的操作,那么最好使用ESP代替EBP。
而且,如果您进行更改ESP,则当然可以相应地更改ESP的偏移量。
add esp,8
mov ecx,[esp-4] //never access data outside the actual stack.
pop eax
sub esp,12
Run Code Online (Sandbox Code Playgroud)
请记住,中断随时可能发生。
该中断将假定可以更改堆栈指针以下的任何内容。如果手动增加堆栈指针,然后访问它下面的数据,就像它仍在堆栈中一样,您可能会发现那里的数据已被中断处理程序(Oops)取代。
规则:ESP以北的任何东西都是安全的,ESP以南的任何东西都被标记为死亡,
这就是例程创建的原因stack frame。通过降低堆栈指针(记住堆栈变小),可以保护一个内存区域,因为它现在位于堆栈内部。
堆栈的语义意味着,ESP之上的任何数据都是安全的,ESP之下的任何数据都是公平的游戏。
如果您通过
A 违反了这两个原则之一,则A-使用非固定的ESP作为基本指针,或者
B 违反了ESP 之下的数据。
您将冒以下风险:A:损坏他人的数据或B:自己使用损坏的数据。
这是不好的做法吗?
add esp,8 //equivalent to pop anyreg, pop anyreg pop eax //pop from the (new) top of the stack. sub esp,12 //reset the stack back to where is was.
如果sub esp,12在更改此堆栈空间位中存储的3个整数之前发生中断,则将导致应用程序中的数据损坏。
请使用以下代码。
mov eax,[esp+8]
Run Code Online (Sandbox Code Playgroud)
这段代码是:A:安全,B:更快,C:不破坏标志寄存器,D:较短,E:编码较少的字节。
有关添加/
订阅的注释如果您在FLAGS中有一些有用的东西,则可以通过使用LEA添加来避免破坏它。如果不是,则add/ sub至少一样快(例如,在某些主流CPU上的更多执行端口上运行,而LEA只能在Ryzen,Haswell及更高版本的4个整数ALU执行单元中的2个上运行)。两种方式都没有代码大小优势。
lea esp,[esp+8] == add esp,8 (but without altering the flags).
lea edx, [esp+8] ; copy-and-add replacing mov + add is very useful
Run Code Online (Sandbox Code Playgroud)
当它可以代替2条或更多条其他指令时,一定要使用LEA,而不仅仅是用来代替添加/订阅,除非您有保留FLAGS的用途。