誤人子弟篇之C語言函式返回值與引數傳遞
寫在開頭以免看到結尾你,此篇部落格純屬瞎扯,看看就可以了,不要當真哦!
如果搞過彙編,寫過子程式,那麼你就不用看了,因為看到最後你會發現,在彙編中你有很多方法去返回值,傳遞引數,而在高階語言中,編譯器只是選擇了其中的一種而已,而這篇部落格也寫的毫無邏輯,簡直喪盡天良,草菅人命,道卻也有那麼點點意思,如果你能看完我叫你大哥,,,
老規矩,先熱下身,簡簡單單的幾行程式碼(額,第一次linux下看AT&T彙編的哦,以前win下intel彙編,但是不管什麼平臺,基本靠猜,,,)
Gcc一下生成a.out ,objdump –Da.out 生成反彙編程式碼,就成芥樣子啦,看不懂吧,我也是,但第一行還是能猜出來的,x86構架下64位elf可執行檔案,我不會,但是我會猜呀,,,
man 一下objdump看一下-D 引數的介紹是檢視elf可執行檔案的所有段
搜尋一下看一下有沒有.data段.bbs段,正如man的介紹-D引數是包含所有段的,在此我們只關注.text段的main函式與fun函式()
搜尋一下main,會發現fun函式就在main函式的上邊,可能跟我宣告定義fun函式的位置有關
通過反彙編程式碼我們可以看到每一個函式進入後都有
push %rbp
mov %rsp,%rbp
但是main函式的程式碼中多了sub $0x10,%rsp ,這行程式碼是幹啥的?學win32彙編時你應該知道,ebp,esp是幹啥的,這裡只是換了個名r代表64位,e代表32位。其實這是給區域性變數分配棧空間,fun也是函式也有區域性變數,為傻沒有這行程式碼?(對比main與fun的程式碼,我猜測可能只有函式內呼叫了函式才會有這行程式碼)。從C程式碼中可以看到我們只在main函式中定義了一個佔用一個位元組的char型別變數,但編譯器卻分配了16個位元組的棧空間(猜測可能是gcc編譯器預設不超過16位元組,會分配16個位元組作為區域性變數的棧空間,其實可能就是這樣)
用實驗證明兩個猜測:
第一個猜測,猜測只有函式內呼叫了函式才會有sub $0x10,%rsp這行程式碼,既然fun函式內沒有函式呼叫,那我們就加一個函式呼叫,修改程式碼如下:
反彙編後如下
喔喔喔,還真是這樣額,一猜一個準其實就是為了給每一個函式呼叫生成一個棧幀而已,,,,
接下來驗證第二個猜想:可能是gcc編譯器預設不超過16位元組,會分配16個位元組作為區域性變數的棧空間
修改程式碼如下:
反彙編一下
通過反彙編程式碼可以看出,在此平臺下一個int型別變數4個位元組,一個short型別變數是3個位元組(我記得書本上說short好像是2個位元組奧,但此處編譯器分配了3個位元組),而一個char型別正如大多數C語言課本上說的是1個位元組。
先不管這個,不管怎麼說我們已經定義了超過16位元組棧空間的區域性變量了,而sub $0x10,%rsp 也變成了sub $0x20,%rsp ,由此我們可以推斷出我們現在使用的gcc編譯器,為區域性變數分配棧空間的一個預設規律,以16位元組的整數倍去為區域性變數分配棧空間,不足16位元組,也分配16位元組,,,
接下來我們再來說位元組問題,由反彙編程式碼可知,雖然編譯器為short型別變數分配了3個位元組棧空間,但為其生成的賦值彙編程式碼卻是movw $0x5,-0x12(%rbp) ,mov後加了一個w字尾,通過各個變數首地址的對比與變數型別可以推斷出:w代表2位元組,b代表1位元組,l代表4位元組,
也就是說,雖然編譯器為short型別變數分配了3個位元組棧空間,但short型別變數還是遵循C語言short型別2個位元組的標準的,生成的彙編程式碼只會去存取首地址開始的2個位元組。雖然這樣但你可不要期望用一個佔用3個位元組的16位的short型別變數可以容納一個最大的24位的變數,你還可以什麼都不用做就可以正確的訪問到這個最大的24位的變數事實真是這樣?不好奕屎這只是我的猜測,還有待測試去證明,,,,
再次觀察反彙編程式碼
4004a6: c745 f4 03 00 00 00 movl $0x3,-0xc(%rbp) int d = 3
4004ad: c645 fb 04 movb $0x4,-0x5(%rbp) char e = 4
4004b1: 66c7 45 fc 05 00 movw $0x5,-0x4(%rbp) short f = 5
4004b7: b800 00 00 00 mov $0x0,%eax
4004bc: e8b3 ff ff ff callq 400474<fun>
4004c1: 8845 ff mov %al,-0x1(%rbp) char b = fun()
你會發現char型別變數e與int型別d變數之間4有3個位元組的空隙,char型別變數b與short型別變數f之間有1個位元組的空隙,這些個位元組是幹啥的?我想起了C語言課本上的位元組對齊,哈哈哈哈啊哈啊哈啊哈,,,,,
修改程式碼測試下,我們用事實說話,不要瞎猜
反彙編對比一下,整整少了16位元組的棧空間,僅僅只是調整了一下char型別與short型別定義的順序而已,想象一下如果我們在此用第一次定義變數的順序去宣告一個結構體或一個c++類,我們在堆中大量建立這個型別的變數,會浪費多少記憶體?
測試一下,修改程式碼如下
執行一下,多了整整4個位元組哦,你要知道玩過51微控制器的程式設計師有多摳門,畢竟才128b的ram,4kb的rom,,,
關於位元組對齊,簡單點就是基本型別所分配的棧空間的首地址要能被自身大小(位元組)所整除,關於結構體對齊可百度,,,
不說了,說了這麼多還沒到正題,你的身熱了?
廢話真多,說好的是看一下,函式是怎麼返回值和傳遞引數的?
整理下發型,現正我們峰迴路轉,言歸正傳,,,
真是柳暗花明另一村!村!村!村!啊!
我們先看一下當函式返回一個位元組的時候gcc編譯器是怎樣生成彙編程式碼的?
讓我們在重溫一下這篇垃圾部落格開始的那幾行看似簡單,其實確實很簡單的幾行程式碼,,,
從1和4可以看到char 型別變數b與a都在main和fun函式呼叫時,編譯器為其分配的棧空間的第一個位元組-0x1(%rbp)處
1:movb $0x1,-0x1(%rbp) 變數賦值語句 char a= 1
2:movzbl-0x1(%rbp),%eax 變數返回語句 returna
3:callq 400474 <fun> 函式fun呼叫語句
4:mov %al,-0x1(%rbp) char b = fun() 接收fun函式返回值
Rax,eax,ax,al,ah:64,32,16,8,8
從1-4我們不難看出,編譯器在返回一個位元組的變數時,會把此變數放到eax暫存器內,然後在返回到主調函式後會把這一個位元組的變數從ax暫存器的低8位也就是1個位元組複製到主調函式的棧空間-0x1(%rbp)也就是接收變數b中
既然一個位元組是這樣返回,那麼2,4,8 個位元組肯定也是這樣返回的,不用測應該肯定就是這樣,因為64位cpu,rax 暫存器有8位元組
那麼問題來了,返回了一個超過了一個暫存器能容納的返回值(返回c++物件(map,list,arry),結構體,陣列),編譯器是怎樣返回的?
不用測,我就知道,返回被調函式中存放此變數棧的首地址(64位下最大的地址也不過8位元組而已)就行了,然後在主調函式中再把此首地址開始的sizeof(此變數)位元組,拷貝到主調函式中接收此變數的棧空間上
事實真是這樣?還是測一下吧
修改程式碼如下
反彙編一下,喔!fun函式太長啦,直接上程式碼
0000000000400581 <main>:
58 400581: 55 push %rbp
59 400582: 48 89 e5 mov %rsp,%rbp
60 400585: 48 83 ec 60 sub $0x60,%rsp
61 400589: 488d 45 a0 lea -0x60(%rbp),%rax
62 40058d: 4889 c7 mov %rax,%rdi
63 400590: b8 00 00 00 00 mov $0x0,%eax
64 400595: e8 da fe ff ff callq 400474 <fun>
65 40059a: c9 leaveq
66 40059b: c3 retq
67 40059c: 90 nop
先來看一下mian函式,發現比呼叫返回一個位元組的fun函式多了61,62兩行,這兩條指令的作用是把-0x60(%rbp)的地址放到rax暫存器(編譯器為接收返回值結構體變數b分配的棧空間首地址),然後再把此地址放到rdi暫存器內
然後我們來看一下fun函式,發現比呼叫返回一個位元組的fun函式多了太多行
0000000000400474 <fun>:
1 400474: 55 push %rbp
2 400475: 48 89 e5 mov %rsp,%rbp
3 400478: 53 push %rbx
4 400479: 4889 fa mov %rdi,%rdx
5 40047c: 488d 5d 90 lea -0x70(%rbp),%rbx
6 400480: b800 00 00 00 mov $0x0,%eax
7 400485: b90c 00 00 00 mov $0xc,%ecx
8 40048a: 4889 df mov %rbx,%rdi
9 40048d: f348 ab rep stos %rax,%es:(%rdi)
10 400490: c7 45 90 01 00 00 00 movl $0x1,-0x70(%rbp)
11 400497: c7 45 94 02 00 00 00 movl $0x2,-0x6c(%rbp)
12 40049e: c7 45 98 03 00 00 00 movl $0x3,-0x68(%rbp)
13 4004a5: c7 45 9c 04 00 00 00 movl $0x4,-0x64(%rbp)
14 4004ac: c7 45 a0 05 00 00 00 movl $0x5,-0x60(%rbp)
15 4004b3: c7 45 a4 06 00 00 00 movl $0x6,-0x5c(%rbp)
16 4004ba: c7 45 a8 07 00 00 00 movl $0x7,-0x58(%rbp
17 4004c1: c7 45 ac 08 00 00 00 movl $0x8,-0x54(%rbp)
18 4004c8: c7 45 b0 09 00 00 00 movl $0x9,-0x50(%rbp)
19 4004cf: c745 b4 0a 00 00 00 movl $0xa,-0x4c(%rbp)
20 4004d6: c7 45 b8 0b 00 00 00 movl $0xb,-0x48(%rbp)
21 4004dd: c7 45 bc 0c 00 00 00 movl $0xc,-0x44(%rbp)
22 4004e4: c7 45 c0 0d 00 00 00 movl $0xd,-0x40(%rbp)
23 4004eb: c7 45 c4 0e 00 00 00 movl $0xe,-0x3c(%rbp)
24 4004f2: c7 45 c8 0f 00 00 00 movl $0xf,-0x38(%rbp)
25 4004f9: c7 45 cc 10 00 00 00 movl $0x10,-0x34(%rbp)
26 400500: c7 45 d0 11 00 00 00 movl $0x11,-0x30(%rbp)
27 400507: c7 45 d4 12 00 00 00 movl $0x12,-0x2c(%rbp)
28 40050e: c7 45 d8 13 00 00 00 movl $0x13,-0x28(%rbp)
29 400515: c7 45 dc 14 00 00 00 movl $0x14,-0x24(%rbp)
30 40051c: 48 8b 45 90 mov -0x70(%rbp),%rax
31 400520: 48 89 02 mov %rax,(%rdx)
32 400523: 48 8b 45 98 mov -0x68(%rbp),%rax
33 400527: 48 89 42 08 mov %rax,0x8(%rdx)
34 40052b: 48 8b 45 a0 mov -0x60(%rbp),%rax
35 40052f: 48 89 42 10 mov %rax,0x10(%rdx)
36 400533: 48 8b 45 a8 mov -0x58(%rbp),%rax
37 400537: 48 89 42 18 mov %rax,0x18(%rdx)
38 40053b: 48 8b 45 b0 mov -0x50(%rbp),%rax
39 40053f: 48 89 42 20 mov %rax,0x20(%rdx)
40 400543: 48 8b 45 b8 mov -0x48(%rbp),%rax
41 400547: 48 89 42 28 mov %rax,0x28(%rdx)
42 40054b: 48 8b 45 c0 mov -0x40(%rbp),%rax
43 40054f: 48 89 42 30 mov %rax,0x30(%rdx)
44 400553: 48 8b 45 c8 mov -0x38(%rbp),%rax
45 400557: 48 89 42 38 mov %rax,0x38(%rdx)
46 40055b: 48 8b 45 d0 mov -0x30(%rbp),%rax
47 40055f: 48 89 42 40 mov %rax,0x40(%rdx)
48 400563: 48 8b 45 d8 mov -0x28(%rbp),%rax
49 400567: 48 89 42 48 mov %rax,0x48(%rdx)
50 40056b: 48 8b 45 e0 mov -0x20(%rbp),%rax
51 40056f: 48 89 42 50 mov %rax,0x50(%rdx)
52 400573: 48 8b 45 e8 mov -0x18(%rbp),%rax
53 400577: 48 89 42 58 mov %rax,0x58(%rdx)
54 40057b: 48 89 d0 mov %rdx,%rax
55 40057e: 5b pop %rbx
56 40057f: c9 leaveq
57 400580: c3 retq
第3行push %rbx說明接下來的程式碼的第5行lea -0x70(%rbp),%rbx會用到rbx暫存器,所以現在要把rbx的值存到fun函式的棧中,我們在函式末可以看到第55行pop %rbx又把原值從棧中談到rbx中啦
第4行mov %rdi,%rdx 把main函式中結構體變數b的首地址放到rdx暫存器中
第5-9行 是初始化棧空間的,大意就是把從此基棧指標-0x70到基棧指標-0x10這段棧空間,分$0xc(12)次,每次初始化8位元組(清0)(32位vc編譯器會用0cccccccch去初始化棧空間)
第10-29行 是為fun函式臨時結構體變數a內的各個變數賦值
接下來重點來了,要開始返回結構體啦
第30行 mov -0x70(%rbp),%rax 把從-0x70(%rbp)地址開始的8個位元組(a,d[0])放到rax暫存器中,別問我是蒸饃知道是8個位元組的,我們可以看一下31,33,35,行指標的變化情況就可以推斷出來啦,而且我們還可以斷定當mov指令的源運算元是一個地址,目的運算元是一個暫存器時,mov指令會移動此地址開始的目的運算元暫存器大小位元組的資料到此暫存器中,,,
第31行 mov %rax,(%rdx) 把rax暫存器中的值(a,d[0])放到rdx儲存的地址上,也就是把fun函式中的臨時結構體變數a的第一個元素a,和第二個元素陣列d的第一個元素d[0],拷貝到main函式中結構體變數b中
接下來的32-53行就是每次8位元組的把,fun函式棧空間內的臨時結構體變數a拷貝到,main函式的棧空間內的臨時結構體變數b中
第54行mov %rdx,%rax 把main函式中棧空間內的臨時結構體變數b的地址放到rax暫存器內(這個有啥用?????)
驗證下來,發現和我的猜想還是有區別的
我想的是被調函式返回臨時變數的地址到主調函式,主調函式再拷貝
而事實是主調函式把接收變數的地址傳到被調函式中,拷貝過程在被調函式中實現,這就好比我們顯示的把接收變數的指標當做引數傳遞給被調函式
這就像c語言的cdel 與c++的 stdcall 一樣,函式呼叫過程中的引數產生的臨時棧空間是由呼叫者清除,還是被呼叫者清除,,,,
這樣設計有一個好處就是在C++中可以直接在return時建立物件,這樣會比先建立物件再返回效率高,直接return 時建立物件,會在主調函式的棧空間中分配記憶體,然後再在此塊記憶體上呼叫建構函式構造物件。而先建立物件,再return ,會在被調函式的棧空間分配記憶體呼叫建構函式構造物件,然後在return時再呼叫物件的拷貝建構函式,把物件拷貝構造到主調函式的棧空間,還要呼叫被調函式臨時物件的解構函式(我只是這樣猜測,不同編譯器實現可能不同,你可以去測一下),,,
其實事實並不是這樣的,你這人怎麼這樣?事實是隻有當被返回的變數的大小大於所有可用的通用暫存器(不同的編譯器,不同的cpu構架下可能會選取幾個通用暫存器用來傳遞引數和返回值,就像此處x86構架下gcc編譯器返回少於8位元組是會用rax,eax等暫存器,vc下會用eax傳遞c++ this指標,g++下會用rdi暫存器傳遞this指標)的大小後才會使用這種方式,
修改程式碼如下驗證一下gcc編譯器是否會用暫存器,傳遞引數and返回值:
structtest{
1 int a[2];
2 char b[2];
3 short c;
};
struct test fun(struct test a)
{
4 return a;
}
void main()
{
5 struct test a={0,1,2,3,4};
6 struct test b= fun(a);
7 b.a[0]=1;
8 b.c=2;
}
0000000000400474 <fun>:
9 400474: 55 push %rbp
10 400475: 48 89 e5 mov %rsp,%rbp
11 400478: 48 89 fa mov %rdi,%rdx
12 40047b: 89 f0 mov %esi,%eax
13 40047d: 48 89 55 e0 mov %rdx,-0x20(%rbp)
14 400481: 89 45 e8 mov %eax,-0x18(%rbp)
15 400484: 48 8b 45 e0 mov -0x20(%rbp),%rax
16 400488: 48 89 45 f0 mov %rax,-0x10(%rbp)
17 40048c: 8b 45 e8 mov -0x18(%rbp),%eax
18 40048f: 89 45 f8 mov %eax,-0x8(%rbp)
19 400492: 48 8b 45 f0 mov -0x10(%rbp),%rax
20 400496: 8b 55 f8 mov -0x8(%rbp),%edx
21 400499: c9 leaveq
22 40049a: c3 retq
000000000040049b <main>:
23 40049b: 55 push %rbp
24 40049c: 48 89 e5 mov %rsp,%rbp
25 40049f: 48 83 ec 30 sub $0x30,%rsp
26 4004a3: c745 f0 00 00 00 00 movl $0x0,-0x10(%rbp)
27 4004aa: c745 f4 01 00 00 00 movl $0x1,-0xc(%rbp)
28 4004b1: c645 f8 02 movb $0x2,-0x8(%rbp)
29 4004b5: c645 f9 03 movb $0x3,-0x7(%rbp)
30 4004b9: 66c7 45 fa 04 00 movw $0x4,-0x6(%rbp)
31 4004bf: 488b 55 f0 mov -0x10(%rbp),%rdx
32 4004c3: 8b45 f8 mov -0x8(%rbp),%eax
33 4004c6: 4889 d7 mov %rdx,%rdi
34 4004c9: 89c6 mov %eax,%esi
35 4004cb: e8 a4 ff ff ff callq 400474 <fun>
36 4004d0: 48 89 c1 mov %rax,%rcx
37 4004d3: 89 d0 mov %edx,%eax
38 4004d5: 48 89 4d d0 mov %rcx,-0x30(%rbp)
39 4004d9: 89 45 d8 mov %eax,-0x28(%rbp)
40 4004dc: 48 8b 45 d0 mov -0x30(%rbp),%rax
41 4004e0: 48 89 45 e0 mov %rax,-0x20(%rbp)
42 4004e4: 8b 45 d8 mov -0x28(%rbp),%eax
43 4004e7: 89 45 e8 mov %eax,-0x18(%rbp)
44 4004ea: c7 45 e0 01 00 00 00 movl $0x1,-0x20(%rbp)
45 4004f1: 66 c7 45 ea 02 00 movw $0x2,-0x16(%rbp)
46 4004f7: c9 leaveq
在mian函式中我們定義了一個12位元組大小的結構體變數a,並初始化了它,然後把變數a當做引數傳給fun函式,並在fun函式中返回此變數到mian函式的結構體變數b中
第26-30行以給定的值初始化結構體變數a
第31行 mov -0x10(%rbp),%rdx 把以-0x10(%rbp)為首地址的8個位元組(int型別陣列a)放到64位rdx暫存器中
第32行 mov -0x8(%rbp),%eax 把以-0x8(%rbp)為首地址的4個位元組(char型別陣列b,和short 型別變數c)放到32位eax暫存器中
第33行 mov %rdx,%rdi 把int型別陣列a放到rdi暫存器
第34行 mov %eax,%esi char型別陣列b,和short 型別變數c 放到esi暫存器中
31-34其實是把12位元組的結構體變數a分8位元組和4位元組分別放到rdi,esi暫存器中當做fun函式的引數(vc6.0使用的編譯器一般會把引數放到棧中而不是用暫存器去傳遞引數,32位系統下傳遞c++this指標是會用eax暫存器去傳遞的)
第11-20行生成了這麼多程式碼,就是為了把rdi中的int型別陣列a放到rax暫存器,把esi暫存器中的char型別陣列b,和short 型別變數c 放到edx暫存器中,作為返回值
第36-43 同11-20是為了把fun函式返回的rax,edx,暫存器中的中值放到mian函式接收結構體變數b中
百思不得其解,明明11-20行和36-43行,2行程式碼就可以搞定的事,為何要搞這麼多程式碼,傳過來,傳過去的,,,
不管怎麼說我們已經看到函式返回值時,gcc編譯器會用暫存器返回值或值太大把接收變數的首地址放到rdi暫存器傳遞到被調函式,然後在被調函式中直接拷貝返回值到接收變數中,傳遞引數時也會使用暫存器,至於什麼時候會用棧去傳遞引數,留給你去測吧,,,
搞到這其實我又有一個猜想,我們在c,c++中返回值時,大必不用宣告返回什麼型別,我們只需要隨便返回一個指標就行了。
1. 因為函式返回前我們並未在反彙編程式碼中看到棧清0操作,這也同時提醒我們在做一些敏感資訊的處理時要記得及時覆蓋敏感資料。
2. 因為程序是作業系統分配資源的最小單位,執行緒是資源排程的最小單位,但執行緒同時還擁有自己的棧空間和被排程執行時佔用的cpu暫存器,棧是執行緒私有的,你線上程內呼叫一個函式後,在下一次呼叫函式之前,把上一個函式呼叫的棧中的東西拷貝出來就行了,不用怕什麼所謂的臨時變數,函式返回後就不存在了,它一直都在,等著你再次在此執行緒內呼叫函式產生棧幀去覆蓋它(在C++中函式返回後棧空間的臨時物件的解構函式會被執行哦),,,,,
這樣我們就可以顯示的編寫程式碼完成函式返回值的拷貝,而不是要編譯器去生成拷貝程式碼
測一下修改程式碼如下
在主調函式main中拷貝fun,fun0,fun1函式返回後,其棧幀中的臨時結構體變數a到主調函式main棧幀中的int型別陣列x中
可以看到編譯器在編譯時會警告,函式返回了一個臨時變數的地址,返回指標型別與接收型別不匹配,懶得理你額,我們看執行結果,函式返回後我們還是可以去訪問這個地址取得變數的值的哦,,,,
我是真的真的是沒騙你的哦!我是真的真的只是在猜測後才驗證的哦,,,
方框1我們不論返回什麼指標我們都可以在函式返回後繼續訪問此函式fun,fun0中的臨時結構體變數a
方框2我們使用memcpy想把函式fun1中的臨時結構體變數a拷貝到main函式中的陣列x中,但執行結果表明我們失敗了,因為我們在拷貝過程中又呼叫了函式,memcpy函式呼叫過程中產生的棧幀覆蓋了fun1函式的棧幀
方框3,4,5 在不呼叫拷貝函式的情況下使用了3種方法,還可以有更多種(其實都是利用變數間的賦值)去拷貝fun1函式中的臨時結構體變數a到main函式臨時陣列x中
可以看到在3,4,5方框我們去呼叫fun1後根本沒接收其返回值,但是我們還是可以通過方框2中的變數b去訪問每一次fun1函式呼叫後的棧幀中的臨時結構體變數a,在方框2中我們第一次呼叫fun1其返回的臨時結構體變數a的地址與其後每一次呼叫fun1中的臨時結構體變數a的地址一直沒改變過,同一個函式在同一個地方無論你重複呼叫多少次其棧幀中各個臨時變數的相對地址都是不會變的,前提是中間沒穿插其它函式呼叫,,,
結論就是:其它我不論你什麼c++,java,python,你什麼編譯型,解釋性,什麼指標,引用,物件,套路就這麼幾樣,最終都會變成cpu指令,只不過虛擬機器會把自己的位元組碼解釋成當前cpu指令,都差不多哦,如果你測了,結果不是,你可以來大石窩,,,
由於編譯器把高階語言中我們所謂的變數,和地址關聯起來了,你可以看到,反彙編中沒什麼變數,全是地址,,,
指標是個好玩的東西,,,,,,