汇编-函数调用栈-内存分配

291 阅读4分钟

1.c语言转汇编

现在有一段c语言代码。

void print1() {
	int a = 0x10;
	if (a != 0) {
		a = 0x100;
	}
	a = 0x1000;
}

int main()
{

	int b = 0x200;
	print1();
	 
	b = 0x300;
}

在main函数中调用了print1函数。 查看print1函数的汇编可以看到。

void print1() {
    push        ebp                ; 函数开头,保存当前函数的栈帧指针
    mov         ebp, esp           ; 设置当前栈帧指针为栈顶指针
    sub         esp, 0CCh         ; 在栈上分配 204 bytes 的空间(用于局部变量和临时存储)
    push        ebx                ; 保存寄存器 ebx,因为它是被调用者保存的寄存器
    push        esi                ; 保存寄存器 esi,因为它是被调用者保存的寄存器
    push        edi                ; 保存寄存器 edi,因为它是被调用者保存的寄存器
    lea         edi, [ebp-0Ch]     ; 将 edi 设置为局部变量 a 的内存地址
    mov         ecx, 3             ; 将 ecx 设置为需要填充的字节数(12 bytes,即 3 个 int)
    mov         eax, 0CCCCCCCCh    ; 将 eax 设置为用于填充的特殊值(通常用于调试)
    rep stos    dword ptr es:[edi] ; 用 eax 的值填充 edi 指向的 12 bytes 内存(初始化局部变量 a)
    
    ; 下面是一些额外的代码,用于调试和检查调试器

    mov         ecx, offset _7535E707_ConsoleApplication3@cpp ; 将 ecx 设置为某个地址
    call        @__CheckForDebuggerJustMyCode@4               ; 调用某个函数

    ; 以下是原始 C 代码的实际汇编代码

    mov         dword ptr [a], 10h  ; 将 a 的值设置为 0x10
    cmp         dword ptr [a], 0    ; 比较 a 的值是否等于 0
    je          __$EncStackInitStart+2Dh ; 如果 a 等于 0,跳转到 __$EncStackInitStart+2Dh(未显示完整代码)
    mov         dword ptr [a], 100h ; 如果 a 不等于 0,将 a 的值设置为 0x100
    mov         dword ptr [a], 1000h ; 将 a 的值设置为 0x1000

    ; 函数结尾
}

2.开辟栈针

前三行指令会初始化栈针 esp ebp。并分配空间。

High Address
+------------------------+
|  Previous Stack Frames  |
+------------------------+
|        Unused          |  <-- Higher address, padding if needed
+------------------------+
|   Local Variables &   |
|     Temporary Data    |
|      (204 bytes)      |
+------------------------+
|  Saved Registers       |  <-- Lower address, such as ebx, esi, edi (if saved)
+------------------------+
|   Return Address       |  <-- Lowest address, points to the instruction after the function call
Low Address

3.接下来会保存寄存器和设置栈空间

4.然后会设置局部变量

现在的内存分布如下。

High Address
+-------------------+
| Saved edi         |  <-- Restored value of edi (popped from the stack)
+-------------------+
| Saved esi         |  <-- Restored value of esi (popped from the stack)
+-------------------+
| Saved ebx         |  <-- Restored value of ebx (popped from the stack)
+-------------------+
|   Local Variables |  <-- Start of local variables and temporary data (allocated space)
|       a (4 bytes) |  <-- Space for 'int a' (local variable)
|       b (4 bytes) |  <-- Space for 'int b' (local variable)
|                   |
|                   |
| Temporary Data   |
|                   |  <-- End of local variables and temporary data
+-------------------+
| Saved Base Ptr    |  <-- Restored value of ebp (popped from the stack)
+-------------------+
| Return Address    |  <-- Return address (address of the instruction after the function call)
Low Address

5.执行业务代码

业务代码是函数中实现具体功能的部分,通常不会直接涉及栈的显式操作。

6.函数结束恢复现场

  • 恢复被调用者保存的寄存器值,即从栈上弹出之前保存的寄存器值。
  • 清理栈空间,即将栈顶指针(esp)恢复到函数调用前的位置,销毁当前函数的栈帧。
  • 恢复上一级函数的栈帧指针(ebp)。

总结

执行完业务代码后,需要按照相反的顺序进行操作,以恢复之前保存的寄存器和清理栈空间。回顾整个过程:

  1. 函数开头保存现场:

    • 将被调用者保存的寄存器值(例如:ebx、esi、edi等)保存到栈上。
    • 设置当前栈帧指针(ebp)为栈顶指针(esp)。
  2. 开辟栈空间分配局部变量和临时存储空间。

  3. 执行业务代码(对局部变量和临时数据进行计算等)。

  4. 函数结束时恢复现场:

    • 恢复被调用者保存的寄存器值,即从栈上弹出之前保存的寄存器值。
    • 清理栈空间,即将栈顶指针(esp)恢复到函数调用前的位置,销毁当前函数的栈帧。
    • 恢复上一级函数的栈帧指针(ebp)。
  5. 执行 ret 指令:从当前函数返回到调用者,并跳转到返回地址。

整个过程确保了函数执行期间的寄存器值和栈的正确管理。函数调用结束后,调用者的现场得到正确恢复,不受被调用函数的影响。