1. 程式人生 > >C語言函式引數入棧的彙編理解

C語言函式引數入棧的彙編理解

先來看這樣一段程式:

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

void print1(int a,int b,int c)
{
    printf("%p\n",&a);
    printf("%p\n",&b);
    printf("%p\n",&c);
}

int main(void)
{
   print1(1,2,3);
   exit(0);
}

它的輸出是:

0022FF40
0022FF44
0022FF48
發現a,b,c的地址是逐漸增大的,差值是4個位元組。這和我所知道的:C函式引數入棧的順序是從右到左 是相匹配的,而且地址的增大值也

與變數所佔的位元組數相匹配。

不過當把程式稍微做一下修改,如下:

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

void print2(char a,char b,char c)
{
   printf("%p\n",&a);
   printf("%p\n",&b);
   printf("%p\n",&c);
}

int main(void)
{
   print2(1,2,3);
   exit(0);
}
再觀察一下它的輸出:
0022FF2C
0022FF28
0022FF24
怎麼和上面的效果是相反的!雖然我知道這肯定編譯器的一個技巧,不過 引數入棧的順序是從右到左的 概念卻動搖了。

為了弄清楚其中的道理,必須觀察程式生成的中間.s檔案,為此,我執行了以下一條命令:

gcc -S test.c(當前C檔案中儲存的程式是文章一開始的那個) 在當前目錄下生成test.s檔案
使用vim開啟test.s檔案(只擷取主要內容了):

esp是指向棧頂的指標,ebp是用來備份這個指標的。棧的形狀如下:

esp

ebp

|____________________________________________________

棧的最大值   棧的最小值

每壓入一個引數入棧,就執行  esp = esp - sizoeof(引數)。不過在esp值變之前,先備份一下 ebp = esp,這樣不管最後esp指到哪裡去了,函式結束時就用這個ebp就能順利回到呼叫者了。

print1:
   pushl %ebp//6.先把ebp壓棧,儲存這個指標
   movl  %esp, %ebp//7.使ebp這個指標儲存著esp這個指標指向的地址值
   subl  $8, %esp//8.使esp - 8,也就是說空下8個位元組以便實現某個功能
   leal   8(%ebp), %eax//9.把(ebp + 8)的地址給eax 這個地方為什麼要+8 因為這個函式在經歷第5,6步的時候存在著壓了兩個4位元組入棧的操作。此時+8就指向了實參1
   movl  %eax, 4(%esp)//10.這個時候就用到第8步空下來的8個位元組中的4個了,原來是儲存值,原理就是用C語言寫兩個數交換值時的那個第三個變數,即緩衝區
   movl  $.LC0, (%esp)//11.把字串“%p\n”壓棧     從第10,11步來看,兩個引數的入棧順序,其實不管順序了,兩個引數,最右邊的在高地址,最左邊的在低地址
   call  printf//12.呼叫函式printf,又是壓棧出棧的操作了到此可以得到8個位元組的緩衝區全部用完了
   leal  12(%ebp), %eax//13.同第9步,此時獲取的是實參2的地址
   movl  %eax, 4(%esp)//14.我想說同上
   movl  $.LC0, (%esp)//15.我想說同上
   call  printf//16.我想說同上
   leal  16(%ebp), %eax//17.同第9步,此時獲取的是實參3的地址
   movl  %eax, 4(%esp)//18.我想說同上
   movl  $.LC0, (%esp)//19.我想說同上
   call  printf//20.我想說同上。到了此處我們就知道,printf列印引數的地址,這個地址是在main函式中壓棧時分配的,是什麼就是什麼,符合引數入棧的順序是從右到左這個說法。
   leave
   ret 
   .size print1, .-print1
.globl main
   .type main, @function
main:
   leal  4(%esp), %ecx
   andl  $-16, %esp
   pushl -4(%ecx)
   pushl %ebp
   movl  %esp, %ebp
   pushl %ecx
   subl  $20, %esp//1.先把棧預留20個位元組,這其中的原因(為什麼是20)估計與什麼演算法有關
   movl  $3, 8(%esp)//2.看!,先把3放入esp + 8
   movl  $2, 4(%esp)//3.再看!,把2放入esp + 4
   movl  $1, (%esp)//4.最後把1放入esp
   call  print1//5.呼叫函式print1。至此可以看到引數1,2,3是從右往左壓入棧的,3在最高最地址(相對於1的儲存地址來說),而1就在最低地址了(相對於3的儲存地址來說)
   movl  $0, (%esp)
   call  exit
   .size main, .-main

好的,這個程式分析完了,再來看有疑問的程式吧:
print2:
   pushl %ebp//5.我想說同上
   movl  %esp, %ebp//6.我想說同上
   subl  $24, %esp//7.這個就不同上了,比上面那個esp - 8大很多嗎,不過要記住,這24個位元組是個緩衝區
   movl  8(%ebp), %eax//8.把實參1放入eax
   movl  12(%ebp), %edx//9.把實參2放入edx
   movl  16(%ebp), %ecx//10.把實參3放入ecx
   movb  %al, -4(%ebp)//11.把eax的低位元組放入ebp - 4
   movb  %dl, -8(%ebp)//12.把edx的低位元組放入ebp - 8
   movb  %cl, -12(%ebp)//13.把ecx的低位元組放入ebp -12。從這個地方就可以發現問題的出現原因了,此時的實參1所存放的地址高於存放實參3的地址。到此,24位元組的緩衝區已經使用了12
   leal  -4(%ebp), %eax//14.把實參1存放的地址放入eax
   movl  %eax, 4(%esp)//15.把實參1放入esp + 4
   movl  $.LC0, (%esp)//16.把字串“%p\n”放入esp
   call  printf//17.呼叫函式printf。從此依然可以看出函式引數的入棧地址是最右邊的在高地址,最左邊的在低地址。到此24位元組的緩衝區使用了20個,還餘下4個沒有用
   leal  -8(%ebp), %eax//18.我想說同上
   movl  %eax, 4(%esp)//19.我想說同上
   movl  $.LC0, (%esp)//20.我想說同上
   call  printf//21.我想說同上
   leal  -12(%ebp), %eax//22.我想說同上
   movl  %eax, 4(%esp)//23.我想說同上
   movl  $.LC0, (%esp)//24.我想說同上
   call  printf//25.我想說同上
   leave
   ret
   .size print2, .-print2
.globl main
   .type main, @function
main:
   leal  4(%esp), %ecx
   andl  $-16, %esp
   pushl -4(%ecx)
   pushl %ebp
   movl  %esp, %ebp
   pushl %ecx
   subl  $20, %esp
   movl $3, 8(%esp)//1.和上面那個程式一樣的壓棧操作
   movl $2, 4(%esp)//2.我想說同上
   movl $1, (%esp)//3.我想說同上
   call  print2//4.我想說同上
   movl  $0, (%esp)
   call  exit
   .size main, .-main

結束了,知道了原因了。這計算機執行函式的時候不停的壓棧出棧,執行這些精細的操作真是太牛了,不過計算機沒有情感,它不會評估這個複雜度,只要一條條執行就行了,就像我們抄作文似的,作文

最後好不好不是我們能決定的,而是作文的作者。我暴露了-_-|||。

謝謝觀賞!

此處的組合語言是AT&T彙編,相關學習資料地址:點選開啟連結

本文參考文章地址:點選開啟連結