c++ linux 下匯編分析傳參以及返回值
註意:都是在沒有優化的情況下編譯的。因為只要開-O1或是-O2,那麽匯編代碼就少的可憐了,都被優化掉了
編譯器版本:x86-64 gcc 5.5
1 POD類型傳參
1.1 一個pod參數,pod返回值
int square(int num) { return num * num; } int main() { int c=90; int a=square(c); ++a; }
對應匯編
1.2 兩個pod參數,pod返回值
int mul(int v1,int v2) { return v1 * v2; } int main() { int c=90; int a=mul(c,1); ++a; }
當第二個參數也傳入變量的時候,會使用edx,像eax一樣傳入。然後返回值依然使用eax返回。
1.3 幾個參數才會動用到棧傳參,
int mul(int v1,int v2,int v3,int v4,int v5,int v6,int v7) { return v1 * v2*v3*v4*v5*v6*v7; } int main() { int c1=90; int c2=10; int c3=10; int c4=10; int c5=10; int c6=10; int c7=10; int a=mul(c1,c2,c3,c4,c5,c6,c7); ++a; c1++;c2++;c3++;c4++;c5++;c6++;c7++; a=mul(a,c2,c3,c4,c5,c6,c7); }
從上圖可以看到,是從第7個參數開始,使用棧傳遞參數。
並且之前都是使用edi作為第一個參數,但是當使用棧的時候就使用edi來倒騰數據了。
註意,棧先回退,然後再去保存返回值
再看函數調用:
2 結構體傳參
class A { public: A() :i1(1) {} public: int i1; char a; int i2; char ca; char c1; ~A() { i2=2; } }; int func(A a1,A a2,A a3,A a4,A a5,A a6,A a7) { a1.a++; return 1; } void func1(int i) { if(i<10) { i++; A a1; A a2; A a3; A a4; A a5; A a6; A a7; int a=func(a1,a2,a3,a4,a5,a6,a7); // a1.a++; // int bb=func(a1,a2,a3,a4,a5,a6); // }else{ // return func1(i+1); // } } }
先看func1函數
傳參
加上一個拷貝構造函數:
A(A& a) { ca=++a.ca; }
然後看看拷貝構造函數的匯編:
對的。你有沒有發現,對象a,也就是參數所在位置的內存,就只有ca成員被初始化了,也就是說被修改了。其他的數據成員都沒有修改。
然後配合
這種,棧指針回退的做法,是不是就能夠明白。為什麽說函數內部的變量,如果沒有初始化,那麽值是未定義的。而不是說0
因為,之前使用這塊內存空間的函數,並沒有將那塊內存空間清0,而是直接sp+8這種形式退了回去。
因此後面函數再使用相同的內存,那麽就是未定義啊!!!未定義啊!
明白是怎麽來的了嗎?
為啥說函數外的變量就不會這樣。因為函數外變量占用的內存就不會被回收,也就不存在被重用。一定是0.它內部的內存一定是0啊~~(那如果是別的進程使用了內存那?操作系統可能會清0吧。這個就真不清楚了)
然後繼續看函數調用
上面 為什麽要 先 rsp -8 ,命名push 的時候可以自動的做到rsp-8。
這裏為啥,我沒想明白。但是當壓入8個參數的時候,也就是說2個參數需要棧傳參,那麽會sp-8*2
再來看一下func函數。改一下,不然有些信息看不出來。
int func(A a1,A a2,A a3,A a4,A a5,A a6,A a7,A b) { b.a++; a1.a++; return 1; }
雖然編譯器是gcc,但是因為有類,因此最終還是用的 g++ 來編譯。
因此,從上面可以看出來了吧,其實參數構造的地方,是在棧上,但是函數實際使用的是 lea 指令取的參數的有效地址,然後保存在寄存器中,被調用函數通過寄存器訪問參數。
也就是說,並不存在什麽值傳遞。值傳遞的本意是參數使用拷貝構造函數復制了一份。然後取其有效地址通過寄存器傳入了被調用函數
那拷貝一份的意義在哪?在於不會更改原來的變量的值。應為傳入的參數是構造函數復制的那份。更改也無所謂。
如果第8個參數改為指針傳參會發生什麽:
首先發生的變化就是,拷貝構造函數的調用少了一次,也就是說第8個參數沒有被拷貝,但是第8個參數需要通過壓棧傳參了,因此可以發現,直接壓棧第八個參數實際值的有效地址
2.2 結構體類型返回值
首先
class A { public: A() : i(1) { } A(A& a) { } int i; // int i1; // int i2; }; A func() { A a; return a; } void func1() { A b = func(); }
這段代碼編譯不通過:
為什麽,因為sp指針在call結束以後直接回退,返回值的處理是在sp指針回退以後才開始進行的。
那也就是說,如果這段代碼可以通過編譯,那麽就是說明,eax寄存器存儲的是返回值的有效地址,而這個有效地址已經在sp指針之下了,也就是說不在棧內了。
換句話說,這個元素已經不可用了。既然已經不可用了,那怎麽還能用一個不可用的變量來構造值??
其實問題是在於,這個值是非const傳遞的,a在棧回退以後是一個匿名變量了,也就是說是一個右值了。她已經不再棧上了(sp之下),因此也就是無法被修改了。
而拷貝構造函數,不可避免的會攜帶之前變量的一些值,也就是說,是可以修改原來的變量的。但是a已經是一個右值了,(其實是在sp之下了),無法被修改。因此編譯器拒絕了這種構造。
但是如果改為
class A { public: A() : i(1) { } A(const A &a) { } int i; // int i1; // int i2; }; A func() { A a; return a; } void func1() { A b = func(); }
編譯就可以通過,為什麽。因為傳入的是const A& 意味著使用這個值構造的對象,不會去修改這個值。或是說不能被修改。因此就可以使用這個值去構造其他對象了。
但是問題還是一個,a已經不再棧上了,怎麽去構造?
答案是先預留出返回值的內存空間,然後將這個地址傳入,在被調用函數中構造。
3 nop是什麽
nop作用
這個,沒看完,不太懂。貼個鏈接吧。
c++ linux 下匯編分析傳參以及返回值