C語言函式引數入棧的彙編理解
阿新 • • 發佈:2019-01-27
先來看這樣一段程式:
#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彙編,相關學習資料地址:點選開啟連結本文參考文章地址:點選開啟連結