我正在阅读有关Stack Frames的Intel manual。有人指出
输入参数区域的末尾应对齐16(32,如果
__m256
在堆栈上传递。字节边界。
我不太明白这意味着什么。这是否意味着rsp
应该指向始终在16上对齐的地址?
我试着用它来试验并编写了一个非常简单的程序:
section .text
global _start
_start:
push byte 0xFF
;SYS_exit syscall
我使用gdb
运行它并注意到在执行push
指令rsp = 0x7fffffffdcf0
之前。它实际上是在16 x/1xg $rsp
返回0x0000000000000001
。
现在,在推送rsp
的内容成为0x7fffffffdce8
之后。这违反了对齐要求吗?
我还注意到x/1xg $rsp
返回了0xffffffffffffffff
。这意味着我们将1
设置为接下来的8个字节,而不仅仅是push指令中指定的字节。为什么?在推到x/1xg $rsp
之后我期望0x00000000000000FF
的输出(我们只推了一个字节)。
答案 0 :(得分:4)
rsp
- 操作系统的入口点,它实际上违反了ABI,因为堆栈应该在_start
之前对齐,因此{{ 1}}本身将添加8B的返回地址,并且您可以预期call
在进入时未对齐-8。
在应用程序输入时,请确保在调用符合ABI的任何其他代码之前手动对齐堆栈(或者如果您计划使用C运行时库,则应用程序代码的入口点应为call
,并且让crtlib让它自己的初始代码在rsp
运行。
现在,在推送
main
的内容成为_start
之后。这违反了对齐要求吗?
是的,如果你在那一点rsp
有一些更复杂的函数,例如0x7fffffffdce8
有非平凡的参数(所以它会使用SSE指令来实现),它很可能是段错误。 / p>
关于call
:
这在64b模式下是不合法的指令(即使在16位和32位模式下也是如此)(printf
操作数目标大小意义上不合法,push byte 0xFF
立即作为源值是合法,但operand size can be only 16, 32 or 64 bits),因此NASM将猜测目标大小(任何来自合法的大小,在64b模式下自然选择byte
),并使用猜测的目标大小与byte
来自源。
在这种情况下,BTW使用qword
选项使NASM发出(有点奇怪,但至少可以调查)警告:
imm8
例如,合法-w+all
只会将两个字节推送到字符串warning: signed byte value exceeds bounds
的堆栈。
如何对齐堆栈:如果您已经知道初始对齐,只需在调用某些需要子程序的ABI之前根据需要进行调整(通常64b代码通常就像不推送任何东西一样简单,或者执行一次多余的推送,如push word 0xFF
)。
如果您不确定对齐方式,请使用一些备用寄存器来存储原始0x00FF
(通常使用push rbp
,因此它也可用作堆栈帧指针),然后rsp
清除底部的位。
请记住,在创建自己的ABI符合子例程时,该堆栈在rbp
之前对齐,因此在进入时为-8B。同样简单的and rsp,-16
通常足以同时解决多个问题,保留call
值(因此push rbp
可以"免费")并对齐堆栈以供休息子程序。
编辑:关于编码,源大小和即时大小...
不幸的是,我并不是100%确定在NASM中应该如何定义它,但我认为实际上rbp
定义是如此复杂,以至于它破坏了NASM语法(令人筋疲力尽当前语法指向您无法指定操作数大小或源即时大小的点,因此默认情况下,大小说明符主要是操作数大小,在某些情况下会影响立即数。)
通过使用mov rbp, rsp
,NASM将push
部分作为"操作数大小",而不仅仅是立即大小。并且push byte 0xFF
不是推送的合法操作数大小,因此NASM将在64b模式下默认选择byte
。然后,它还会将byte
视为即时大小,并将qword
签名扩展为byte
。即这对我来说是一种未定义的行为。 NASM创建者可能不希望你指定即时大小,因为NASM会对大小进行优化,因此当你执行0xFF
时,它会将其组合为"推送操作数imm8"。你可以用另一种方式覆盖它,以确保你qword
获得imm16。
查看由各种组合产生的机器代码(在64b模式下)(其中一些严格说话至少值得警告,甚至错误,例如"严格qword"仅产生imm32,而不是imm64 (因为当然不存在imm64操作码)...甚至没有提到push word -1
变体实际上是push strict word -1
操作数大小,你不能在64b模式下使用32b操作数大小):< / p>
dword
无论如何,我想没有太多人为此烦恼,因为在64b模式下你通常需要qword push(qword
)并以最短的方式立即编码,所以你只需要写 6 00000000 6AFF push -1
7 00000002 6AFF push strict byte 0xFF
8 ****************** warning: signed byte value exceeds bounds
9 00000004 6AFF push byte 0xFF
10 ****************** warning: signed byte value exceeds bounds
11 00000006 6AFF push strict byte -1
12 00000008 6AFF push byte -1
13 0000000A 6668FF00 push strict word 0xFF
14 0000000E 6668FF00 push word 0xFF
15 00000012 6668FFFF push strict word -1
16 00000016 666AFF push word -1
17 00000019 68FF000000 push strict dword 0xFF
18 0000001E 68FF000000 push dword 0xFF
19 00000023 68FFFFFFFF push strict dword -1
20 00000028 6AFF push dword -1
21 0000002A 68FF000000 push strict qword 0xFF
22 0000002F 68FF000000 push qword 0xFF
23 00000034 68FFFFFFFF push strict qword -1
24 00000039 6AFF push qword -1
和让NASM处理rsp -= 8
优化本身,期望push -1
当然要改变-8。在其他情况下,他们可能希望您了解合法的操作数大小,而不是使用imm8
。
如果您认为这是不可接受的,我会在NASM论坛/ bugzilla /某处提出这个问题,它应该如何正常工作。就我个人而言,目前的行为已经足够好了#34;对我来说(有道理,加上我不时快速查看列表文件,以验证机器代码字节中没有令人讨厌的惊喜,并按预期落地)。也就是说,我主要是代码大小的介绍,所以我知道生成的每个字节及其目的。如果NASM突然产生rsp
而不是预期的byte
,我会在二进制大小上看到它并进行调查。