小练习:通过反汇编一段C代码观察栈行为

这是我的嵌入式笔记第二篇,原文写于2015年。

深入了解计算机的底层行为是软件工程师的内功之一,有助于理解程序代码是如何被执行的。本文的练习小而完整,不需要太多背景知识,因此摘选出来与大家分享。

目的

  • 在學習 Linux kernel 分析的基礎課程時有分析到 C 代碼執行時 stack 的行爲,那麼這裏做一個最簡單的實驗,通過把 C 代碼反彙編成 ASM 代碼,來看堆棧的操作情況。
  • 備註:
    • 直接用的 64-bit 環境,gcc -S -o main.s main.c(因爲網路大多都是32bit的分析,照做沒新意)
    • 也可以用 GDB 來看,覺得有點殺雞用牛刀XDD

源代碼和編譯出來的代碼

  • 源代碼:

  • 反編譯出來的代碼(64-bit):

分析

  • 首先,可以看到一件事是:編譯出來的組合語言是 32-bit 和 64-bit 混合的,應該是針對寄存器的 bit 不同指令做的相應的使用
  • 指令從第14行開始也就是 main 函數第一條指令開始執行,首先是把%rbp寄存器的值壓入棧內,保存當前工作的棧基指
  • 接着把rsp的地址賦給rbp,含義是初始化一個新的 stack
  • 第16行是預設的空間,GCC下-O1指令似乎可以優化
  • 第17、18行將a(1,2)中的兩個參數分別存到esiedi寄存器中,這個地方需要說明的是 x86-32 中會使用 push 將實參存入 stack 內,如下圖(原main.c反編譯成 32-bit ASM 代碼):
    • 而在 x86-64 中,如果參數在6個以內,GCC 就可以利用寄存器來存儲參數值;超過6個的參數,還是通過上述操作實現,這裏給我們了至少兩個啓發:
      • 儘量使用6個以下的參數列表
      • 傳大的數值時儘量使用指針或引用,因爲寄存器只有64位並且只能存整形數值
  • 然後就呼叫 a 函數了,call其實也分爲兩個動作:
    • 第一個是把當前rip的值保存
    • 把 a 函數的指令初始地址給rip
  • a 函數也是同理,先保存當前的rbp然後初始化新的 stack
  • 第4、5行,就將保存在esiedi內的值按順序放入-24(%ebp)-20(%ebp)
  • 這可以說明,在X86-64中,實參入棧和賦值給形參都是在子函數代碼段完成的
  • 第6到8行就是從 stack 中取出參數到通用寄存器完成加法運算
  • 第9、10行是算出的值會先從eax存到-4(%rbp),即給 tmp 變量賦值
  • 然後再將tmp的值給eax寄存器,函數的返回值默認使用eax寄存器返回給上一級函數
  • 第11行將rbp出棧,然後第12行ret就是把之前保存的rip出棧
  • 回到第20行指令,將eax的值給-4(%rbp),也就是給變量 i
  • 爲了return 0把0給eax
  • 因爲是main函數,所以最後是leave(leave: Releases the local stack storage created by the previous ENTER instruction.)

流程圖

Ref