在各種計算機程式語言中,都少不了函式呼叫來完成某些特定功能。我們要使用乙個函式前,必定會先定義該函式(可能還會先宣告它)。在這個過程中,我們大多會給這個函式傳遞某些『引數』,當我們定義被呼叫函式時,函式名後的括號( )中即為我們傳入的「形參」,函式可以在函式體中描述如何操作這些「形參」;同時,我們呼叫函式時,例如 fun(param1,param2,...),括號裡面的這些param,我們稱之為實參。簡單來說,呼叫函式時可以把實參傳給定義的函式作為形參以供函式來使用它們。
形參和實參的定義確實很好理解,但如果僅僅是理解它們是什麼的話,這往往是不夠的。
接下來,我會在系統堆疊層面說明一下函式呼叫過程中,形參與實參以及函式區域性變數是如何在堆疊裡變化的。
先說乙個c語言中的例子,如果我們想要通過乙個子函式交換在主函式main()裡面定義的兩個區域性變數x,y的值的話,一些初學者可能會像這樣寫code:
寫完之後,在主函式列印了一下x,y發現x與y的值根本沒有變化啊!
在這裡,我們在主函式中定義了兩個int型別的區域性變數x和y,它們的值分別為3和5,然後我們呼叫了在主函式前面定義的交換函式exchange,並將變數x與y分別作為了兩個實參傳遞給它;再看exchange函式中,我們的得到了兩個傳過來的形參a、b,然後我們通過同乙個中間變數使它們(注意,兩個形參)交換。得到的結果時,主函式的x,y的值並沒有發生交換。
這個**正確的寫法應該是:
這次我們的實參是&x,&y,即傳遞過去的是x,y的位址,相當傳遞過程中存在int *a=&x與int *b=&y操作,其結果是讓a指向x,b指向y。然後同樣通過tmp完成值交換。
如果我們單單從指標方面去考慮為什麼這次能夠交換成功,可能還不太容易弄懂交換的本質,接下來讓我談一談這次交換在系統堆疊中是如何做到的。
眾所周知,計算機儲存體系分為4層,它們的讀寫速度從慢到快依次是外存《記憶體《快取記憶體其中,暫存器又可分類為通用暫存器,段暫存器,輔助暫存器,兩個特殊暫存器:ip暫存器和flag標誌位暫存器
所有的c源程式都要進行編譯,從而生成最終的機器指令**和檔案,那麼,上述的內容最終是由編譯軟體負責完成的;也就是說,編譯軟體將我們的ascii碼形式的源程式**,根據自己的原則,使用上述的暫存器完成源**的任務。反過來說,如果在程式設計中需要特殊處理,那麼,就需要給編譯器一定的「指導」,方可達到我們的目的!
輔助暫存器中有兩個暫存器bp(基址暫存器)和sp (棧暫存器),通常bp表示系統堆疊的棧底位址,sp表示系統堆疊的棧頂位址。
函式呼叫的過程中就發生了如下的變化:
首先,我們將**的執行過程顯示成以.asm為字尾的彙編檔案
可以看見,當我們呼叫exchange函式時,我們將x與y的位址作為實參變數傳遞了過去,在彙編層面,發生的事情是先通過lea指令將ebp[x]和ebp[y]位址的偏移量,把它們分別傳送到通用暫存器裡的dx和cx暫存器裡面。然後通過call指令呼叫函式exchange.也就是說,呼叫函式時,我們將距離ebp(棧底位址)為4個和8個位元組的x和y變數位址偏移量,傳送給ecx和edx暫存器;有人可能想問,上圖中為什麼_x$=-8,_y$=-12。我們都知道,堆疊是一種後進先出的資料結構,區域性變數定義後,會使堆疊記憶體減小,位址往低處走,區域性變數相對於棧底指標bp在其上面,也就是堆疊的低處,所以偏移量用負值表示。同時,主函式其實也是乙個相對於作業系統的子函式,作業系統呼叫主函式,然後主函式再呼叫子函式。也就是說,建立主函式時,我們在主函式中定義的相對於主函式的區域性變數也同樣被壓入了儲存變數空間的系統堆疊。接下來我將通過主函式呼叫子函式的例子說明:
子函式是被call指令執行的,call指令能夠停止主函式的執行,並進入子函式,執行其函式體內容。當然,還要保護主函式的執行狀態資訊,以使子函式呼叫完成返回後能夠接著執行主函式後面的操作!進入子函式後,先將ebp入棧,這樣做是為了保護主函式的ebp資訊。第二步則是將esp移到ebp,棧頂指標和棧底指標重合,形成了乙個空棧。而這個空棧就是為exchange函式中區域性變數所準備的空間。exchange函式不僅擁有傳進來的形參變數a,b,還有自己定義的區域性變數tmp。
第三步的push ecx,是為區域性變數tmp準備的,這個操作後,esp向位址低處移動4b,相當於偏移量為-4;
與此同時,堆疊裡的情況如下圖所示:
實參變數是呼叫函式exchange時入棧的,而且是根據括號裡的順序($x,$y)從右往左依次入棧。而賦值給形參變數是從左往右賦值的,也就是上面所說的int *a=&x與int *b=&y。往上,我們還要push兩個名為ebp和eip的值入棧,ebp入棧是因為我們要儲存main原ebp的值,而為什麼這裡還有4b的eip值也入棧了呢?這是因為,eip值是用來存放「下一步"要執行的某個程序(執行緒)的**的位址的,主函式呼叫子函式,主函式被」中斷「,當子函式返回到主函式時,同時也應該準確地返回到主函式被中斷的地方,繼續往下執行。所以eip是保護」現場資訊「的關鍵。於是,這也就解釋了為什麼形參變數a和b與ebp的偏移量分別為8b和12b了。如果子函式在其內部繼續定義區域性變數,那麼空間就繼續往上走,esp指標也跟著往上走就可以了。函式呼叫的入棧順序就很明了了:實參表示式(形參)->main()eip->main()ebp->被調函式區域性變數。如果子函式中再呼叫其它函式,就如printf(),入棧細節也是如法炮製的。
但是,這好像依舊沒搞清楚最開始的問題,即它們是如何成功交換值的。要搞清楚這個,我們就必須明白主函式區域性變數,實參和形參三者的所占用空間關係。其重點就是,區域性變數與實參並不是占用同一空間,然而實參表示式算出來的值,在系統堆疊占用的空間,就是與之對應的形參變數的空間。也就是說,如果我們按開頭第一種**寫交換功能的話,我們交換的只是,形參變數空間中的值而已,因為它只相當於複製了乙份主函式中x,y的值給形參而已,並沒有產生能夠改變真正在主函式中x,y的值。
但是如果我們傳遞給exchagne的是x,y的位址的話,就相當於構建了exchange中形參變數a和b與主函式x,y的一種位址指向」聯絡「。一旦我們建立了這種聯絡,我們就可以將a所指向空間的值(區域性變數x的值)與b所指向空間的值(區域性變數y的值)同過tmp進行交換。從而達成了交換的功能。
c語言中形參與實參的關係
一 形參出現在函式定義中,在整個函式體內都可以使用,形參變數只有在被呼叫時才分配記憶體單元,在呼叫結束時即刻釋放所分配的記憶體單元,因此,形參只有在函式內部有效。函式呼叫結束返回主調函式後則不能再使用該形參變數。離開該函式則不能使用,實參出現在主調函式中,進入被調函式後,實參變數也不能使用。二 形參...
C語言基礎 形參與實參
上課老師會給你講函式的形參與實參的概念,前期基本上背誦,沒有真正理解。這裡寫乙個簡單的例子說明下。測試用例如下 arm linux gcc test para.c o test para 這裡用交叉編譯工具,方便後面讀彙編 測試結果,很明顯real swap func才能達到交換的目的 簡單的說你得...
C語言 函式的形參與實參
形參出現在函式定義中,在整個函式體內都可以使用,離開該函式則不能使用。實參出現在主調函式中,進入被調函式後,實參變數也不能使用。形參和實參的功能是作資料傳送。發生函式呼叫時,主調函式把實參的值傳送給被調函式的形參從而實現主調函式向被調函式的資料傳送。1.形參變數只有在被呼叫時才分配記憶體單元,在呼叫...