先來看這樣一段程序:
#include#include #include void print1(int a,int b,int c) { printf(%p ,&a); printf(%p ,&b); printf(%p ,&c); } int main(void) { print1(1,2,3); exit(0); }
它的輸出是:
0022FF40 0022FF44 0022FF48發現a,b,c的地址是逐漸增大的,差值是4個字節。這和我所知道的:C函數參數入棧的順序是從右到左 是相匹配的,而且地址的增大值也
與變量所占的字節數相匹配。
不過當把程序稍微做一下修改,如下:
#include再觀察一下它的輸出:#include #include void print2(char a,char b,char c) { printf(%p ,&a); printf(%p ,&b); printf(%p ,&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 ”壓棧 從第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 ”放入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