c語言程序運行時的棧與寄存器的變化


原創作品轉載請注明出處

參考材料 《Linux內核分析》 MOOC課程http://mooc.study.163.com/course/USTC-1000029000 ”

作者:Casualet

 

我們在這里從匯編代碼的角度, 給出一段簡單的C語言程序運行過程中機器狀態的變化情況. 我們的實驗環境是Ubuntu 64位, 編譯器gcc的版本是4.8.4. 

我們使用的c程序如下:

  1. int g(int x){
  2. return x + 3;
  3. }
  4. int f(int x){
  5. return g(x);
  6. }
  7. int main(void){
  8. return f(8) + 1;
  9. }

這個簡單的c程序有一個main函數, 在main函數里調用了f函數, 然后f函數調用了g函數. 我們把其編譯成32位的匯編代碼, 使用的命令是:  gcc -S -o main.s main.c -m32. 這樣,我們獲得了匯編代碼文件main.s,  打開以后可以看到這種效果:

  1. .file "test.c"
  2. .text
  3. .globl g
  4. .type g, @function
  5. g:
  6. .LFB0:
  7. .cfi_startproc
  8. pushl %ebp
  9. .cfi_def_cfa_offset 8
  10. .cfi_offset 5, -8
  11. movl %esp, %ebp
  12. .cfi_def_cfa_register 5
  13. movl 8(%ebp), %eax
  14. addl $3, %eax
  15. popl %ebp
  16. .cfi_restore 5
  17. .cfi_def_cfa 4, 4
  18. ret
  19. .cfi_endproc
  20. .LFE0:
  21. .size g, .-g
  22. .globl f
  23. .type f, @function
  24. f:
  25. .LFB1:
  26. .cfi_startproc
  27. pushl %ebp
  28. .cfi_def_cfa_offset 8
  29. .cfi_offset 5, -8
  30. movl %esp, %ebp
  31. .cfi_def_cfa_register 5
  32. subl $4, %esp
  33. movl 8(%ebp), %eax
  34. movl %eax, (%esp)
  35. call g
  36. leave
  37. .cfi_restore 5
  38. .cfi_def_cfa 4, 4
  39. ret
  40. .cfi_endproc
  41. .LFE1:
  42. .size f, .-f
  43. .globl main
  44. .type main, @function
  45. main:
  46. .LFB2:
  47. .cfi_startproc
  48. pushl %ebp
  49. .cfi_def_cfa_offset 8
  50. .cfi_offset 5, -8
  51. movl %esp, %ebp
  52. .cfi_def_cfa_register 5
  53. subl $4, %esp
  54. movl $8, (%esp)
  55. call f
  56. addl $1, %eax
  57. leave
  58. .cfi_restore 5
  59. .cfi_def_cfa 4, 4
  60. ret
  61. .cfi_endproc
  62. .LFE2:
  63. .size main, .-main
  64. .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4"
  65. .section .note.GNU-stack,"",@progbits

由於以點開頭的都是鏈接時候用到的信息, 跟實際的代碼執行邏輯沒有關系, 為了方便分析,我們給出刪除了以點開頭的行以后的代碼版本:

  1. g:
  2. pushl %ebp
  3. movl %esp, %ebp
  4. movl 8(%ebp), %eax
  5. addl $3, %eax  
  6. popl %ebp
  7. ret
  8. f:
  9. pushl %ebp
  10. movl %esp, %ebp
  11. subl $4, %esp
  12. movl 8(%ebp), %eax
  13. movl %eax, (%esp
  14. call g
  15. leave
  16. ret
  17. main:
  18. pushl %ebp
  19. movl %esp, %ebp
  20. subl $4, %esp
  21. movl $8, (%esp)
  22. call f
  23. addl $1, %eax
  24. leave
  25. ret

在這里,我們可以清晰地看到匯編代碼和三個3函數之間的對應關系.我們補充兩張代碼的圖例:

接下來我們從main函數開始分析:

首先是

  1. pushl %ebp
  2. movl %esp, %ebp
  3. subl $4, %esp
這三條指令的作用是保存棧的信息. 我們將棧想象成一段內存空間, 其中ebp寄存器指向棧底位置, esp寄存器指向棧頂位置,棧底位置是高地址,棧頂位置是低地址.  當進入main函數時,需要使用一段新的棧空間, 也就是說, 如果原來棧是如圖1的棧空間, 現在,進入main函數后,執行了上面的3條指令,變成了圖2到圖3的情況.

      圖1

               圖2

                圖3

這樣, 從ebp開始,到esp 就是屬於main函數的棧. main函數執行完, 需要清空這個棧, 返回原來的狀態, 但是怎么返回呢? 因為我們保存了100這個信息, 所以我們知道, 在調用main函數以前,ebp的值是100, esp的值是88, 所以我們可以返回. 這也就是為什么要做上面這三個步驟. 然后我們繼續執行指令, 把數字8放在esp指向的位置, 得到如下的結果:

                圖4

接下來,調用函數f, 這一步會把eip壓棧. eip指向的是call的下一條指令, addl $1, %eax. 進入f函數以后, 又進行以下三步:

  1. pushl %ebp
  2. movl %esp, %ebp
  3. subl $4, %esp

這個的效果和前面講的是一樣的, 結果圖如下:

                圖5

然后,movl  8(%ebp), %eax 表示把ebp+8地址所在位置的值放到eax中, 在這里,這個值是正好是8. (對應c語言,我們發現原來要做的事情是int x參數傳遞.所以說, 在32位的x86情況下, 函數的參數傳遞是通過棧來實現的, 我們在使用call 指令調用函數前, 先把函數需要的參數壓棧, 然后再使用call指令導致eip壓棧, 然后進入新的函數后, 舊的ebp壓棧, 新的ebp指向的位置存了這個剛壓棧的舊的ebp. 所以, 我們通過新的ebp指向的位置, 可以通過計算的方法, 得到函數需要的參數).  接下來, movl %eax, (%esp)  會把eax的值放到esp指向的內存的位置, 然后調用 g函數, 又可以壓棧call指令的下一條指令的地址, 得到的結果圖是:

                圖6

然后,我們進入了g函數, 執行了前兩條指令,得到的結果是:

                圖7

第三條指令, 和前面說過的用法相同, 是把8這個數字放在%eax中.下一個指令把數字+3,所以現在eax中的數字是11.  接下來的popl %ebp, ebp的值變成了72,因為這個時候esp執行的位置存放的值就是72,這個值正好就是之前存放的上一個函數的ebp的值, 所以得到如下的圖:

                圖8

然后, ret執行,會把leave的地址彈到eip中, 就可以執行leave 指令了.得到的圖是: 

                圖9

leave 指令類似一條宏指令, 等價於

   movl %ebp, %esp

   popl %ebp

我們知道,ebp=72指向的位置存了82這個數,正好是上一次存的舊的ebp的值, 所以經過這步得到如下的圖.

                圖10

這樣, 又遇到了一次ret, 開始執行main 函數中的addl $1, %eax, 由於eax 的值是11, 所以現在變成了12. 然后又碰到leave 指令, 達到清棧的目的, 效果圖如下:

                圖11

於是, 棧恢復了初始的狀態. 我們可以看到, 在main函數之后, 有一個ret指令. 由於我們之前進入main函數的時候沒有考慮地址壓棧, 那部分是操作系統來管理的,  所以這里不考慮這條指令的執行.

總結:

一個函數的執行過程, 會有自己的一段從ebp 到esp的棧空間.  對於一個函數, ebp指向的位置的值是調用這個函數的上一個函數的棧空間的ebp的值. 這種機制使得leave指令可以清空一個函數的棧, 達到調用之前的狀態. 由於在這個棧設置之前, 有一個eip壓棧的過程, 所以leave 以后的ret正好對應了上一個函數的返回地址, 也就是返回上一個函數時要執行的指令的地址. 另外,由於對於一個函數的棧空間來說, ebp指向的位置存了上一個ebp的值, 再往上是上一個函數的返回地址, 再往上是上一個函數壓棧傳參數的值, 所以我們知道了自己的當前ebp, 就可以通過棧的機制來獲得參數.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM