函數在程式設計語言中可謂“頭等公民”,了解函數的實作原理,函數的一些方法論對于程式設計非常有好處。 我将從函數的實作原理以及編寫函數的一些建議兩個的角度來重新認識一下C、C++中的函數。 那具體函數在彙編層面到底是什麼,以及函數是如何跳轉的。本文嘗試從下面從彙編的角度去了解一下c函數。
首先是一段比較簡單的C代碼,我編譯成彙編,然後解讀每一個彙編指令到底做了什麼操作。
C代碼如下
在我Mac 64位機器編譯後的彙編主要代碼為
這裡先複習下彙編知識,下面會經常提及
1.進入main函數邏輯
從位址為<code>0x100000f20</code> 開始main函數邏輯
第一步為将前一棧幀的棧基位址rbp入棧,第二部為将棧頂位址rsp拷貝至rbp中。
完成這一步後,就完成了保留上一幀的基位址,初始化本幀的棧頂位址。
這裡以我debug的位址為例,此時rbp 的值為 0x730,rsp值也為0x730

2. 配置設定棧空間
接下來rsp減去0x30 (48)個位元組,即棧頂向低位元組移動48個位元組,變成0x700,相當于目前棧幀為目前的函數配置設定了48個位元組的空間,用于存放函數局部參數。目前函數執行完後,rsp回到上一函數的棧頂,便達到了回收局部變量的功能。
此時的棧資訊如下
3.為局部變量指派
接着下面6個指令為局部變量指派。前面3個命名暫時忽略,由第4個指令開始,分别是将立即數0xb (11) 寫入到 rbp往低位址偏移0x14位元組的記憶體塊中。将立即數0x16 (22) 寫入到 rbp往低位址偏移0x18位元組的記憶體塊中。将立即數0x21 (33) 寫入到 rbp往低位址偏移0x1c位元組的記憶體塊中。這也就是C函數中局部變量指派操作x=11;y==22;z=33;
此時的棧
4. 傳遞參數
接下來的三個指令非常簡單,便是将上一步驟中的三個全局變量x,y,z移動至寄存器 edi,esi,edx中。
看到這裡便有一個疑問,其實做一個傳遞立即數的操作,為什麼需要先傳遞到記憶體,再傳遞到寄存器用于函數調用呢?這是因為movl 的操作數不能是立即數,是以必須要先将立即數傳遞到記憶體區域,再從記憶體區域傳遞至寄存器。
5.函數跳轉
callq 的操作為下一條指令的位址(0x100000f59)入棧,然後跳轉至 0x100000f00。跳轉後rip為0x100000f00
6.子函數調用
将前一個堆棧的棧基位址寄存器rbp入棧。rsp向低位址偏移8個位元組。
并且将rsp指派給rbp。
由此開始便是子函數的棧幀。此時rbp和rsp是相同。
7.擷取形參與計算
到這裡便是從剛才的edi 中取出x , 從esi中取出y ,從edx取出y,分别放置到rbp偏移0x4,0x8,0xc的記憶體中。
并将三者相加将結果放置eax中。
8 子程式跳出函數,跳轉回到main函數
執行前的堆棧
最後便是回到main函數的步驟。第一個指令将棧頂的資料出棧,并且将其指派給rbp。從上步驟中可以看到,棧頂資料其實便是0x730,即main函數的棧底。
下一步執行ret ,繼續将棧頂出棧,并且将值付給rip。按照rip此時訓示的指令位址繼續執行程式
執行指令後
到此,子程式便退出,回到了main函數的prinf函數中,繼續執行。
1.避免在非排程函數中使用控制函數
在日常程式設計中,有時會非常自然的根據一些配置參數,來實作具體的功能,也很自然的在函數中根據參數的值的不同,函數體内将不同情況的分支情況都寫在一起。
排程函數指根據輸入的消息類型或者控制指令來啟動相應功能實體。
簡單而言,便是根據配置,調用其他功能函數,其本身隻關心“what to do”。
而非排程函數(功能函數)實作具體的某個功能,其本身關心“hot to do”。
以此為規則可以清晰的将函數進行備援的函數進行分層。
例如以下:
這裡使用了一個calu_flg參數進行加減法的區分。這種方式其實是非常的不合理,違背了函數實作單一功能的原則。
如下是将排程函數與非排程函數(功能函數)進行區分
2.使用const防止指針類型變量被修改
如果參數僅作為輸入,則使用const修飾符聲明,防止函數修改該值
3. 函數如無傳回值時,顯式聲明void類型的傳回
聽起來其實非常簡單,日常程式設計中也不容易遺漏。這裡提及一下C的早期版本中,支援不填傳回值。且預設的傳回值為int。
如下的函數聲明在某些版本下是可以正常編譯
4.確定函數入口與出口的安全性
入口即參數的合法性。以”永不信任的原則“,對傳入的參數合法性進行校驗。
出口即return的傳回值必須涵蓋所有的正常與異常情況。
在使用其他函數時,也需要對調用函數的傳回值進行判斷,同時也需對錯誤的傳回值進行相應的錯誤處理。
5.局部變量不易過多
人類大腦同時記住的7個不同的東西,超過這個就會犯糊塗。是以局部變量的數目應該少,應該不差過5-10個
1 .函數的棧的實作其實是修改來rbp與rsp的實作的。通過控制這個兩個寄存器在函數調用前儲存前一函數的rbp壓棧,函數體執行完成後出棧回退至上一個函數的rbp,來達到函數調用的效果。
2 . 函數的局部變量是通過移動rsp的值而配置設定的。函數退出時,rsp回到前函數的棧頂,這便達到了函數推出時,局部變量也随之釋放的效果。
3 .對于函數的功能架構而言,應該遵從功能與排程的分離,盡量做到各盡其事。
4 .對于函數體内的個個switch與if等的分支邏輯,應該先主後次,先正常邏輯再異常邏輯。