x86 32位反汇编中栈对齐与函数参数访问的疑问
重要说明
本问题与ARM汇编无关,添加Android标签仅为环境说明。
我了解汇编基础,但刚接触反汇编,尤其对汇编中函数参数的访问存在疑惑。汇编中通常使用EBP(帧指针)访问局部变量,因其在函数栈帧执行期间保持静态,而ESP会不断递减,因此通过EBP加固定偏移访问函数参数更简便。
环境信息
使用Android Termux环境
安装proot-distro运行Linux(Ubuntu)ELF文件
使用交叉编译器i686-linux-gnu-gcc编译x86 32位ELF文件
使用x86_64-linux-gnu-objdump进行反汇编
疑惑的指令
在反汇编结果中,我注意到以下指令:
endbr32 LEA ECX, [ESP+ 0x4] AND ESP, 0xfffffff0 PUSH DWORD PTR [ECX- 0x4]
我理解AND ESP, 0xFFFFFFF0指令:通过将ESP与掩码0xFFFFFFF0进行按位与,清零ESP的低四位,将栈对齐到16字节边界。这是因为movaps等SSE指令要求XMM寄存器中的打包数据类型需16字节对齐,非对齐操作(如movups)会降低性能;同理AVX-2要求YMM寄存器256位(32字节)对齐,AVX-512要求ZMM寄存器512位(64字节)对齐。
但我对栈对齐指令前后的两条指令的作用存在疑惑:
LEA ECX, [esp + 0x4] PUSH DWORD PTR [ECX- 0x4]
我的解读
LEA ecx, [esp + 0x4]指令将ESP上方4字节的地址加载到ECX中。
PUSH DWORD PTR [ecx - 0x4]指令将调用者的返回地址压入对齐后的栈中,确保调用指令前栈帧对齐。
观察结论
参数访问偏移受影响
由于对齐空间的存在,函数参数相对于EBP的偏移不再固定。例如常规汇编中可通过EBP+8等固定偏移访问参数,但16字节对齐后,偏移量取决于返回地址的栈地址,导致无法再使用固定偏移访问参数。
例如根据返回地址的不同,对齐可能占用2个或3个DWORD,无法固定。
示例程序1
#include <stdio.h> int main(int argc, char **argv) { printf("Count: %d\n", argc); return 0; }
main函数反汇编结果
0000118d <main>: 118d: f3 0f 1e fb endbr32 1191: 8d 4c 24 04 lea ecx, [esp + 0x4] 1195: 83 e4 f0 and esp, 0xfffffff0 1198: ff 71 fc push DWORD PTR [ecx - 0x4] 119b: 55 push ebp 119c: 89 e5 mov ebp, esp 119e: 53 push ebx 119f: 51 push ecx 11a0: e8 2c 00 00 00 call 11d1 <__x86.get_pc_thunk.ax> 11a5: 05 33 2e 00 00 add eax, 0x2e33 11aa: 89 ca mov edx, ecx 11ac: 83 ec 08 sub esp, 0x8 11af: ff 32 push DWORD PTR [edx] 11b1: 8d 90 30 e0 ff ff lea edx, [eax - 0x1fd0] 11b7: 52 push edx 11b8: 89 c3 mov ebx, eax 11ba: e8 81 fe ff ff call 1040 <printf@plt> 11bf: 83 c4 10 add esp, 0x10 11c2: b8 00 00 00 00 mov eax, 0x0 11c7: 8d 65 f8 lea esp, [ebp - 0x8] 11ca: 59 pop ecx 11cb: 5b pop ebx 11cc: 5d pop ebp 11cd: 8d 61 fc lea esp, [ecx - 0x4] 11d0: c3 ret
各指令执行后的栈布局
初始栈布局
Argument 2 (Top): [ESP + 8] (0xbfff1230) Argument 1: [ESP + 4] (0xbfff122c) Return Address: [ESP] (0xbfff1228)
LEA ECX, [ESP + 4]执行后栈布局
Argument 2 (Top): [ESP + 8] (0xbfff1230) Argument 1: [ESP + 4] (0xbfff122c) Return Address: [ESP] (0xbfff1228)
ECX -> 第一个参数的地址
AND ESP, 0xFFFFFFF0执行后栈布局
Argument 2 (Top): [OLD ESP + 8] (0xbfff1230) Argument 1: [OLD ESP + 4] (0xbfff122c) Return Address: [OLD ESP] (0xbfff1228) Alignment DWORD 1: (0xbfff1224) Alignment DWORD 2: (0xbfff1220) <- ESP
ESP -> 0xbfff1220
PUSH DWORD PTR [ECX - 4]执行后栈布局
Argument 2 (Top): [OLD ESP + 8] (0xbfff1230) Argument 1: [OLD ESP + 4] (0xbfff122c) Return Address: [OLD ESP] (0xbfff1228) Alignment DWORD 1: (0xbfff1224) Alignment DWORD 2: (0xbfff1220) Return Address: <- ESP (0xbfff121C)
ESP -> 0xbfff121C
结合上述栈布局,我认为ECX被加载了[ESP + 4]的地址(即第一个参数的地址),由于对齐导致无法用EBP访问参数,ECX被用于存储函数参数的起始地址。请问我的理解是否正确?
示例程序2
#include <stdio.h> int main(int argc, char **argv) { printf("Count: %d\n", argc); printf("Filename: %s\n", *argv); return 0; }
反汇编结果
0000118d <main>: 118d: f3 0f 1e fb endbr32 1191: 8d 4c 24 04 lea ecx, [esp + 0x4] 1195: 83 e4 f0 and esp, 0xfffffff0 1198: ff 71 fc push DWORD PTR [ecx - 0x4] 119b: 55 push ebp 119c: 89 e5 mov ebp, esp 119e: 56 push esi 119f: 53 push ebx 11a0: 51 push ecx 11a1: 83 ec 0c sub esp, 0xc 11a4: e8 e7 fe ff ff call 1090 <__x86.get_pc_thunk.bx> 11a9: 81 c3 2f 2e 00 00 add ebx, 0x2e2f 11af: 89 ce mov esi, ecx 11b1: 83 ec 08 sub esp, 0x8 11b4: ff 36 push DWORD PTR [esi] 11b6: 8d 83 30 e0 ff ff lea eax, [ebx - 0x1fd0] 11bc: 50 push eax 11bd: e8 7e fe ff ff call 1040 <printf@plt> 11c2: 83 c4 10 add esp, 0x10 11c5: 8b 46 04 mov eax, DWORD PTR [esi + 0x4] 11c8: 8b 00 mov eax, DWORD PTR [eax] 11ca: 83 ec 08 sub esp, 0x8 11cd: 50 push eax 11ce: 8d 83 41 e0 ff ff lea eax, [ebx - 0x1fbf] 11d4: 50 push eax 11d5: e8 66 fe ff ff call 1040 <printf@plt> 11da: 83 c4 10 add esp, 0x10 11dd: b8 00 00 00 00 mov eax, 0x0 11e2: 8d 65 f4 lea esp, [ebp - 0xc] 11e5: 59 pop ecx 11e6: 5b pop ebx 11e7: 5e pop esi 11e8: 5d pop ebp 11e9: 8d 61 fc lea esp, [ecx - 0x4] 11ec: c3 ret
从反汇编结果可见:
第二个参数的偏移量基于ECX指向的基地址计算。此处使用了ESI + 4,而ESI的值来自ECX,随后将ESI + 4的值移入EAX并压栈。
因此我认为LEA ecx, [esp + 0x4]指令用于保存函数参数的起始地址(即第一个参数的地址),以便在函数内部访问参数。请指正该理解是否正确。
内容的提问来源于stack exchange,提问作者Nalan PandiKumar

