简单的栈操作与函数调用

简单的栈操作与函数调用

汇编语言基础:编码风格、栈操作与函数调用

汇编语言是计算机底层的编程语言,直接与硬件交互。掌握汇编语言不仅有助于理解程序的运行机制,还能优化性能。本文将介绍汇编语言的编码风格、栈操作以及函数调用的实现,并通过示例代码详细解析。

汇编编码风格

汇编语言主要有以下几种编码风格:

1. Intel 风格

操作数顺序:目标操作数, 源操作数。

示例:mov eax, ebx ; 将 ebx 的值移动到 eax

add eax, 5 ; 将 eax 的值加 5

特点:

寄存器名称和指令助记符通常是小写。

直观易读,适合初学者。

2. AT&T 风格

操作数顺序:源操作数, 目标操作数。

示例:movl $5, %eax ; 将立即数 5 移动到 eax

addl %ebx, %eax ; 将 ebx 加到 eax

特点:

寄存器名前加 %,立即数前加 $,内存地址用 () 表示。

主要用于 Unix/Linux 系统。

3. NASM 风格

类似于 Intel 风格,但语法更严格。

示例:mov eax, 5

add eax, ebx

特点:

常用于编写 Linux 内核模块或引导程序。

objdump 工具

objdump 是一个反汇编工具,用于将二进制文件(如可执行文件或目标文件)转换为汇编代码。常用选项:

-d:反汇编代码段。

-M intel:指定使用 Intel 风格的汇编语法。

-M att:指定使用 AT&T 风格的汇编语法。

示例:

objdump -d -M intel test64 > output.asm

将 test64 文件反汇编为 Intel 风格的汇编代码,并保存到 output.asm 文件中。

示例代码解析

以下是一个完整的汇编代码示例,展示了函数调用、栈操作和平栈的过程:

0000000000001129 :

1129: f3 0f 1e fa endbr64 ; 用于Intel CET的指令,防止ROP攻击

112d: 55 push rbp ; 保存旧的基址指针

112e: 48 89 e5 mov rbp,rsp ; 设置新的基址指针

1131: 89 7d fc mov DWORD PTR [rbp-0x4],edi ; 保存第一个参数

1134: 89 75 f8 mov DWORD PTR [rbp-0x8],esi ; 保存第二个参数

1137: 89 55 f4 mov DWORD PTR [rbp-0xc],edx ; 保存第三个参数

113a: 89 4d f0 mov DWORD PTR [rbp-0x10],ecx ; 保存第四个参数

113d: 44 89 45 ec mov DWORD PTR [rbp-0x14],r8d ; 保存第五个参数

1141: 44 89 4d e8 mov DWORD PTR [rbp-0x18],r9d ; 保存第六个参数

1145: 8b 55 fc mov edx,DWORD PTR [rbp-0x4] ; 将第一个参数加载到edx

1148: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] ; 将第二个参数加载到eax

114b: 01 c2 add edx,eax ; 将eax加到edx

114d: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc] ; 将第三个参数加载到eax

1150: 01 c2 add edx,eax ; 将eax加到edx

1152: 8b 45 f0 mov eax,DWORD PTR [rbp-0x10] ; 将第四个参数加载到eax

1155: 01 c2 add edx,eax ; 将eax加到edx

1157: 8b 45 e8 mov eax,DWORD PTR [rbp-0x18] ; 将第六个参数加载到eax

115a: 01 c2 add edx,eax ; 将eax加到edx

115c: 8b 45 10 mov eax,DWORD PTR [rbp+0x10] ; 将第七个参数(栈上的参数)加载到eax

115f: 01 c2 add edx,eax ; 将eax加到edx

1161: 8b 45 18 mov eax,DWORD PTR [rbp+0x18] ; 将第八个参数(栈上的参数)加载到eax

1164: 01 d0 add eax,edx ; 将edx加到eax

1166: 5d pop rbp ; 恢复旧的基址指针

1167: c3 ret ; 返回

0000000000001168

:

1168: f3 0f 1e fa endbr64 ; 用于Intel CET的指令,防止ROP攻击

116c: 55 push rbp ; 保存旧的基址指针

116d: 48 89 e5 mov rbp,rsp ; 设置新的基址指针

1170: 6a 08 push 0x8 ; 将第八个参数(8)压入栈

1172: 6a 07 push 0x7 ; 将第七个参数(7)压入栈

1174: 41 b9 06 00 00 00 mov r9d,0x6 ; 将第六个参数(6)放入r9d

117a: 41 b8 05 00 00 00 mov r8d,0x5 ; 将第五个参数(5)放入r8d

1180: b9 04 00 00 00 mov ecx,0x4 ; 将第四个参数(4)放入ecx

1185: ba 03 00 00 00 mov edx,0x3 ; 将第三个参数(3)放入edx

118a: be 02 00 00 00 mov esi,0x2 ; 将第二个参数(2)放入esi

118f: bf 01 00 00 00 mov edi,0x1 ; 将第一个参数(1)放入edi

1194: e8 90 ff ff ff call 1129 ; 调用func函数

1199: 48 83 c4 10 add rsp,0x10 ; 清理栈上的参数

119d: b8 00 00 00 00 mov eax,0x0 ; 将返回值设置为0

11a2: c9 leave ; 恢复栈帧

11a3: c3 ret ; 返回

栈操作与函数调用

1. 入栈操作

在函数调用时,参数和局部变量通常会被保存到栈中。以下代码将寄存器中的参数保存到栈中:

1131: 89 7d fc mov DWORD PTR [rbp-0x4],edi ; 保存第一个参数

1134: 89 75 f8 mov DWORD PTR [rbp-0x8],esi ; 保存第二个参数

1137: 89 55 f4 mov DWORD PTR [rbp-0xc],edx ; 保存第三个参数

113a: 89 4d f0 mov DWORD PTR [rbp-0x10],ecx ; 保存第四个参数

113d: 44 89 45 ec mov DWORD PTR [rbp-0x14],r8d ; 保存第五个参数

1141: 44 89 4d e8 mov DWORD PTR [rbp-0x18],r9d ; 保存第六个参数

入栈的原因:

保存寄存器状态:

函数调用时,寄存器可能被覆盖,因此需要将参数保存到栈中,以便后续使用。

例如,DWORD PTR [rbp-0x4] 表示将 edi 的值保存到栈中,地址为 rbp-0x4。

DWORD PTR 表示操作的数据大小为 4 字节(32 位),rbp-0x4 是栈中的一个偏移地址。

支持递归调用:

每次函数调用都会有自己的栈帧,保存参数和局部变量,避免数据冲突。

遵循调用约定:

在 x86-64 架构中,前 6 个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9),多余的参数通过栈传递。

保存到栈中是为了统一处理,确保函数能够正确访问所有参数。

2. 平栈操作

平栈(Stack Alignment)是指在函数调用结束后,恢复栈指针 rsp 到调用前的状态,以保持栈的平衡。通常通过调整 rsp 的值来实现。

示例:

1199: 48 83 c4 10 add rsp,0x10

这行代码将栈指针 rsp 增加 16 字节(0x10),用于清理栈上之前压入的两个 8 字节参数(push 0x8 和 push 0x7)。

平栈后,栈指针恢复到调用函数前的状态,保持栈的平衡。

平栈的原因:

保持栈的完整性:函数调用时,参数和局部变量会占用栈空间。调用结束后,需要释放这些空间,否则会导致栈指针错误。

避免栈溢出:如果不平栈,栈空间会逐渐被耗尽,最终导致栈溢出。

遵循调用约定:在 x86-64 架构中,调用者负责清理栈上的参数。

堆栈图

以下是 main 函数调用 func 时的堆栈图:

+-------------------+

| 返回地址 | <-- rsp (调用 func 前)

+-------------------+

| 旧 rbp | <-- rbp (main 函数的栈帧)

+-------------------+

| 局部变量/参数 |

+-------------------+

| 参数 8 (0x8) | <-- rsp+0x10

+-------------------+

| 参数 7 (0x7) | <-- rsp+0x8

+-------------------+

| 参数 6 (0x6) | <-- r9d

+-------------------+

| 参数 5 (0x5) | <-- r8d

+-------------------+

| 参数 4 (0x4) | <-- ecx

+-------------------+

| 参数 3 (0x3) | <-- edx

+-------------------+

| 参数 2 (0x2) | <-- esi

+-------------------+

| 参数 1 (0x1) | <-- edi

+-------------------+

总结

汇编编码风格:主要有 Intel、AT&T 和 NASM 风格,objdump 工具用于反汇编二进制文件。

入栈操作:保存参数和局部变量,避免寄存器被覆盖,并支持递归调用。

平栈操作:恢复栈指针,保持栈的完整性,避免栈溢出,并遵循调用约定。