-
1 # 仨星貳姨
-
2 # 江南一散人
根據所用編譯器和CPU的不同,以及返回值資料型別的不同,C語言中的函式返回值可能透過暫存器傳遞,也可能透過棧傳遞。對大多數CPU和編譯器來說,出於效能考慮,能使用暫存器傳遞的,儘量使用暫存器傳遞,只有當暫存器不夠用的時候,才會透過棧傳遞。
針對這兩種情況,我分別舉個x64 + GCC環境下的例子來說明。
透過暫存器傳遞返回值如下圖中的一段簡單的程式碼,返回值是一個有符號整數型別
我們看下x64/GCC下面對應的彙編程式碼:
test函式中的
1129: mov $0x2,%eax
便是把返回值2存放到eax暫存器中。而main函式中的
113d: callq 1125 <test>
1142: mov %eax,-0x4(%rbp)
則先呼叫test函式,然後把返回值從eax中取出,並存放到rbp - 4的地址處,也就是賦值給區域性變數a。
透過棧傳遞返回值下面這個例子中,test()函式返回一個結構體struct result。(注:這裡只是為了演示用棧傳遞返回值,實際專案中不建議函式直接返回結構體,可以用結構體指標代替)
(這個例子第一眼看上去會有些許複雜,千萬不要懵逼,彙編程式碼不是洪水猛獸,掌握一些基本的彙編程式碼對修煉內功、除錯問題都是大有裨益的:)
在x64/GCC環境下的彙編程式碼如下:
先看main()函式:
我們先看main()函式中呼叫test()的幾條指令:
ret = test();
11dd: lea -0x50(%rbp), %rax
11e1: mov %rax, %rdi
11e4: mov $0x0, %eax
11e9: callq 1135 <test>
11dd和11e1兩條指令的作用是把棧地址rbp - 0x50存放到rdi暫存器中,我們暫且不去管這個地址是用來做什麼的,等看了test()函式之後自然就會明白。後面兩條指令是把eax清零,然後呼叫test()函式。
test()函式的彙編程式碼如下:
test()的彙編看起來是不是有點複雜呢?不要緊張,其實做的事情很簡單,就是給區域性變數r分配棧空間,然後對它進行初始化,然後把r的值存放到一個記憶體地址當中,最後把這個記憶體地址放到rax暫存器中,並返回出去。我們仔細分析一下:
1139: mov %rdi, -0x28(%rbp)
這條指令是把rdi暫存器的值存放到棧空間rbp - 0x28的地址處。還記得rdi暫存器中存放的是什麼嗎?回想一下,在main()函式呼叫test()函式之前,是不是把一個地址存放到rdi暫存器中了呢?忘了的話,再去看一下。我們先不管這個值用來做什麼,只要記得,test()函式把main()函式傳遞過來的一個值存放到了一個棧地址當中。
接下來的這幾條指令,就是對區域性變數r進行初始化:
struct result r = {1, 2, 3, 4};
113d: movq $0x1, -0x20(%rbp)
1145: movq $0x2, -0x18(%rbp)
114d: movq $0x3, -0x10(%rbp)
1155: movq $0x4, -0x8(%rbp)
下面就要把r的值返回出去了,我們來看看編譯器是怎麼做的,先看這幾條指令:
return r;
115d: mov -0x28(%rbp), %rcx
1161: mov -0x20(%rbp), %rax
1165: mov -0x18(%rbp), %rdx
1169: mov %rax, (%rcx)
116c: mov %rdx, 0x8(%rcx)
115d這條指令,是把棧中rbp-0x28處的值放到rcx暫存器中,還記得這個地址存放的值是什麼嗎?對了,就是test()入口處從rdi中取出來的那個值,也就是main()函式透過rdi暫存器傳遞給test()的一個值。然後,1161和1169兩條指令把r.a值存放到rcx暫存器指向的地址處,1165和116c兩條指令把r.b的值存放到rcx暫存器指向的地址再偏移8的位置處。
現在我們再來回過頭想一下,main()函式透過rdi暫存器傳遞給test()函式的那個值是用來做什麼的呢?對了,那個值其實就是存放test()函式返回值的那塊記憶體的地址。
那麼記下來的幾條指令就比較容易理解了:
1170: mov -0x10(%rbp), %rax
1174: mov -0x8(%rbp), %rdx
1178: mov %rax, 0x10(%rcx)
117c: mov %rdx, 0x18(%rcx)
1170和1178把r.c存放到rcx + 0x10地址處,1174和117c把r.d存放到rcx + 0x18地址處。
到這裡為止,test()函式已經把區域性變數struct result r的所有欄位的值全部存放到main()函式透過rdi暫存器傳遞給test()的那個記憶體地址中。
最後,看一下剩下的幾條指令:
1180: mov -0x28(%rbp), %rax
1184: pop %rbp
1185: retq
1180指令把rbp - 0x28處的值rax中,也就是把存放返回值的那塊記憶體的地址,存放到rax暫存器中,最後返回出去。
到這裡,是不是清晰多了呢?我們再來總結一下這個過程:
main()函式把一個棧空間中的地址rbp - 0x30透過rdi暫存器傳遞給test()函式test()函式從rdi暫存器中取得這個地址,然後把要返回的值存放到這個地址指向的記憶體中test()把這個地址存放到rax暫存器中,並返回給main()函式掌握一定彙編知識的重要性可能對於很多童鞋來說,組合語言比較晦澀難懂,難以掌握。確實,作為一個最為接近機器語言的程式語言來說,彙編確實比較晦澀,除了一些做底層系統軟體的童鞋外,日常工作中直接用匯編寫程式碼的機會確實不多,但是,這並不意味著掌握組合語言就毫無用處。
掌握一定的彙編知識,會對整個計算機的原理和體系結構有更深入的理解,很多東西都能夠知其然並知其所以然。尤其那些對底層系統軟體感興趣的童鞋,如BIOS/bootloader、OS核心、裝置驅動、編譯器、虛擬機器等,組合語言更是必須要掌握的。有些做上層應用的童鞋,如前端開發等,平時用到彙編的機會不多,但是在除錯一些問題的時候,如果能夠了解一些彙編知識,就會如虎添翼,事半功倍。
總之,不管所用的開發語言是C/C++還是Java、Python、PHP、Javascript,不管是做系統軟體開發,還是做前端開發,只要是有志於幹程式設計師這一行當的,掌握一定的彙編,對完善自己的技術知識體系,增強自己除錯問題的能力,和對計算機體系結構的理解都大有裨益。
思考題能堅持讀到這裡,我想你已經基本清楚C語言的函式返回值是怎麼傳遞的了。
-
3 # 和不同
簡單的說:
函式的返回值在函式返回時有效
函式內的區域性變數,退出函式時立刻失效
相應的,要注意:
不要期望在函式外訪問函式的區域性變數:例如在函式中定義區域性陣列,並將陣列指標返回呼叫者,這一指標在返回時已經失效,很可能指向其它資料。
若需要從函式中獲取較多、較複雜的資料,可透過傳遞指標引數的方式實現:例如C庫函式中的sprintf
-
4 # Ac桃邑
堆和棧都在記憶體內
普通變數放在堆空間,傳遞的臨時數值放在棧空間,傳遞完畢之後棧資料出棧之後無法訪問
除非你定義static變數,這個變數會存放在堆內
回覆列表
一般a=fun(),函式執行後,返回值在暫存器,立碼是個賦值運算,把值從暫存器或暫存器指向的棧空間複製到變數的記憶體空間。(返回結構會比較複雜)但都不是啥要注意的。一般說法也不是返回main,是返回呼叫