匯編中的函數調用與遞歸

棧幀的結構

?

  倘若我們要想搞清楚過程的實現,就必須先知道棧幀的結構是如何構成的。棧幀其實可以認為是程序棧的一段,而程序棧又是存儲器的一段,因此棧幀說到底還是存儲器的一段。那么既然是一段,肯定有兩個端點,這個不需要LZ再普及了吧。

  這兩個端點其實就是兩個地址,一個標識著起始地址,一個標識著結束地址,而這兩個地址,則分別存儲在固定的寄存器當中,即起始地址存在%ebp寄存器當中,結束地址存在%esp寄存器當中。至于為什么要存在這兩個寄存器當中,就像程序的下一條指令地址為什么存在PC當中一樣,是毫無意義的問題,就是這樣規定的,沒有為什么。

  起始地址和結束地址還有另外的名字,起始地址通常稱為幀指針,結束地址通常稱為棧指針(也就是棧頂的地址)。因此,我們就把過程的存儲器內存使用區域稱為棧幀。這下我們就了解了棧幀的來歷以及它們的命名習慣和存儲慣例,接下來是LZ畫的一幅圖,它揭示了棧幀在存儲器當中的位置。

  這個圖基本上已經包括了程序棧的構成,它由一系列棧幀構成,這些棧幀每一個都對應一個過程,而且每一個幀指針+4的位置都存儲著函數的返回地址,每一個幀指針指向的存儲器位置當中都備份著調用者的幀指針。各位需要知道的是,每一個棧幀都建立在調用者的下方(也就是地址遞減的方向),當被調用者執行完畢時,這一段棧幀會被釋放。還有一點很重要的是,%ebp和%esp的值指示著棧幀的兩端,而棧指針會在運行時移動,所以大部分時候,在訪問存儲器的時候會基于幀指針訪問,因為在一直移動的棧指針無法根據偏移量準確的定位一個存儲器位置。

  還有一點比較重要的內容,就是棧幀當中內存的分配和釋放。由于棧幀是向地址遞減的方向延伸,因此如果我們將棧指針減去一定的值,就相當于給棧幀分配了一定空間的內存。這個理解起來很簡單,因為在棧指針向下移動以后(也就是變小了),幀指針和棧指針中間的區域會變長,這就是給棧幀分配了更多的內存。相反,如果將棧指針加上一定的值,也就是向上移動,那么就相當于壓縮了棧幀的長度,也就是說內存被釋放了。需要注意的是,上面的一切內容,都基于一個前提,那就是幀指針在過程調用當中是不會移動的。

?

過程的實現

?

  過程雖然很好,但想要實現過程,還是存在一定難度的,盡管現在看來它并不困難。它實現的難度主要就在于數據如何在調用者和被調用者之間傳遞,以及在被調用者當中局部變量內存的分配以及釋放。

  不過天大的難題都難不倒那群計算機界的大神們,他們找出了一種方式,可以簡單并有效的處理過程實現當中的難題。這一切似乎看起來十分偶然,但其實也是必然的。世間的很多規律都是客觀存在的,只是它在等著我們去發現而已。

  總的來說,過程實現當中,參數傳遞以及局部變量內存的分配和釋放都是通過以上介紹的棧幀來實現的,大部分情況下,我們認為過程調用當中做了以下幾個操作。

  1、備份原來的幀指針,調整當前的幀指針到棧指針的位置,這個過程就是我們經常看到的如下兩句匯編代碼做的事情。

    pushl    %ebpmovl    %esp, %ebp

  2、建立起來的棧幀就是為被調用者準備的,當被調用者使用棧幀時,需要給臨時變量分配預留內存,這一步一般是經過下面這樣的匯編代碼處理的。

    subl    $16,%esp

  3、備份被調用者保存的寄存器當中的值,如果有值的話,備份的方式就是壓入棧頂。因此會采用如下的匯編代碼處理。

    pushl    %ebx

  4、使用建立好的棧幀,比如讀取和寫入,一般使用mov,push以及pop指令等等。

  5、恢復被調用者寄存器當中的值,這一過程其實是從棧幀中將備份的值再恢復到寄存器,不過此時這些值可能已經不在棧頂了。因此在恢復時,大多數會使用pop指令,但也并非一定如此。

  6、釋放被調用者的棧幀,釋放就意味著將棧指針加大,而具體的做法一般是直接將棧指針指向幀指針,因此會采用類似下面的匯編代碼處理(也可能是addl)。

    movl    %ebp,%esp

  7、恢復調用者的棧幀,恢復其實就是調整棧幀兩端,使得當前棧幀的區域又回到了原始的位置。因為棧指針已經在第六步調整好了,因此此時只需要將備份的原幀指針彈出到%ebp即可。類似的匯編代碼如下。

    popl    %ebp

  8、彈出返回地址,跳出當前過程,繼續執行調用者的代碼。此時會將棧頂的返回地址彈出到PC,然后程序將按照彈出的返回地址繼續執行。這個過程一般使用ret指令完成。

  過程的實現大概就是以上八個步驟組成的,不過這些步驟并不都是必須的(大部分時候,開啟編譯器的優化會優化掉很多步驟),而且第6和第7步有時會使用leave指令代替。這里猿友們可以先了解一下這些步驟,在接下來的內容當中,還會有這幾個步驟的詳細示例。

?

過程相關指令:call、leave、ret

?

  由于過程調用當中會經常見到幾個新的指令,因此在這里,LZ先給大家介紹一下這三個指令。它們三個都是過程實現當中非常重要的角色,這三個指令很類似,因為它們都是一個指令做了兩件事,這里LZ就依次介紹一下它們各自都做了什么事。

  call指令:它一共做兩件事,第一件是將返回地址(也就是call指令執行時PC的值)壓入棧頂,第二件是將程序跳轉到當前調用的方法的起始地址。第一件事是為了為過程的返回做準備,而第二件事則是真正的指令跳轉。

  leave指令:它也是一共做兩件事,第一件是將棧指針指向幀指針,第二件是彈出備份的原幀指針到%ebp。第一件事是為了釋放當前棧幀,第二件事是為了恢復調用者的棧幀。

  ret指令:它同樣也是做兩件事,第一件是將棧頂的返回地址彈出到PC,第二件事則是按照PC此時指示的指令地址繼續執行程序。這兩件事其實也可以認為是一件事,因為第二件事是系統自己保證的,系統總是按照PC的指令地址執行程序。

  可以看出,除了call指令之外,leave和ret指令都與上面8個步驟有些不可分割的關系。call指令沒有在8個步驟當中體現,是因為它發生在進入過程之前,因此在第1步發生的時候,call指令往往已經被執行了,并且已經為ret指令準備好了返回地址。

?

寄存器使用的規矩

?

  寄存器一共就8個,因此在數目上來說的話,使用起來肯定是捉襟見肘的。在這種情況下,就肯定需要一定的規矩去約束程序如何使用,否則要是一群人翻同一個人的牌子,那到底伺候誰才是呢。其實我們在之前已經或多或少的接觸到了寄存器的規矩,比如%eax一般用于存儲過程的返回值,%ebp保存幀指針,%esp保存棧指針。這里要介紹的,是另外一個規矩,而這個規矩是與過程實現相關的。

  試想一下,在調用一個過程時,無論是調用者還是被調用者,都可能更新寄存器的值。假設調用者在%edx中存了一個整數值100,而被調用者也使用這個寄存器,并更新成了1000,于是悲劇就發生了。當過程調用完畢返回后,調用者再使用%edx的時候,值已經從100變成了1000,這幾乎必將導致程序會錯誤的執行下去。

  為了避免上面這種情況發生,就需要在調用者和被調用者之間做一個協調。于是便有了這樣的規矩,它的描述如下,我們假設這里在過程P中調用了過程Q,P是調用者,Q是被調用者。

  %eax、%edx、%ecx:這三個寄存器被稱為調用者保存寄存器。意思就是說,這三個寄存器由調用者P來保存,而對于Q來說,Q可以隨便使用,用完了就不用再管了。

  %ebx、%esi、%edi:這三個寄存器被稱為被調用者保存寄存器。同樣的,這里是指這三個寄存器由被調用者Q來保存,換句話說,Q可以使用這三個寄存器,但是如果里面有P的變量值,Q必須保證使用完以后將這三個寄存器恢復到原來的值,這里的備份,其實就是上面那8個步驟中第3個步驟做的事情。

?

一個過程示例

?

  上面已經做好了充足的準備,接下來我們就要探索真理了,我們隨便寫一個調用過程的例子,LZ寫了以下的代碼來做這個十分重要的例子,我們稱它為function.c。

復制代碼
int add(int a,int b){register int c = a + b; return c; } int main(){ int a = 100; int b = 101; int c = add(a,b); return c; }
復制代碼

  這里LZ為了完整的展現那8個步驟,因此給變量c加了register關鍵字修飾,這將會將c送入寄存器,從而更改被調用者保存寄存器,就會導致步驟3的發生。接下來我們就使用參數-S來編譯這段代碼,然后使用cat來看看這段代碼的匯編形式。以下是main函數以及add函數各自的棧幀情況,LZ已經詳細標記了它們屬于哪個步驟。

  由于我們沒有使用編譯優化,因此匯編代碼會多出很多,這也為了完整的詮釋我們的步驟。可以看到,圖中包含了完整的8個步驟,但是無論是main函數還是add函數,它們單獨來講,都沒有完整的8個步驟,這其實是大多數的情況。大部分時候,一個函數不會完全包含上述的8個步驟。LZ這里不再一一拆分各個步驟,各位猿友可以嚴格按照各個指令的作用,自己畫圖理解一下這個過程,答案自會浮現。

  LZ這里只說幾點各位需要注意的地方,首先第一點是,add函數會將返回結果存入%eax(前提是返回值可以使用整數來表示),在main函數中,call指令之后,默認將%eax作為返回結果來使用。第二點是,所有函數(包括main函數)都必須有第1步和第6、7、8步,這是必須的4步。最后一點是,我們的棧指針和幀指針有固定的大小關系,即棧指針永遠小于等于幀指針,當二者相等時,當前棧幀被認為沒有分配內存空間。

  還有一點十分有趣的事情,注意main函數當中100和101的傳遞過程,是先進入存儲器,然后再進去寄存器,然后再進去存儲器,準備作為add函數的參數。這一來一回產生了四次寄存器與存儲器之間的數據傳輸,倘若我們加上-O1參數去編譯這個程序,編譯器將產生如下的匯編代碼。

  可以看到,整個main函數的指令數驟降,100和101將直接進入存儲器,準備作為add函數的參數。可見編譯器的優化當中至少會有一項,就是減少數據的來回傳輸,增加效率。不過這一點其實與過程的實現沒有什么關系,只是讓以前可能不知道的猿友看一下,編譯器其實會將我們的程序做很大的改動。

  

遞歸過程調用

?

  書中對遞歸調用還進行了說明,這是為了讓我們相信,棧幀的建立和銷毀慣例,可以保證遞歸過程的正常運行。其實如果各位猿友愿意一點一點的,將上面main函數和add函數的匯編代碼搞清楚,那么遞歸調用其實也可以很輕松的搞定。因為指令就這么多了,只要嚴格按照-S編譯出的匯編指令,一步一步的推算寄存器和存儲器的狀態,那么遞歸調用的實現也會自動浮現。

  LZ這里準備給各位猿友詮釋一下遞歸的過程,各位猿友可以對照著上面的示例看一下,以下是一段簡單的求n的階乘的代碼。

復制代碼
int rfact(int n){int result; if(n<=1){ result = 1; }else{ result = n * rfact(n-1); } return result; }
復制代碼

  接下來我們編譯一下這段代碼,使用-O1優化,我們可以得到如下的匯編代碼。

  LZ在圖中詳細標注了各個步驟所做的事情,其實嚴格按照各個指令的作用分析,很輕松的就可以分析出圖中的解釋部分(即注釋)。難點就在于,棧幀的變化是如何的,LZ這里就給各位演示一下棧幀的變化過程,如果各位已經把前面的那個main函數和add函數搞定了,那么可以在這里驗證一下自己的理解是否正確。

  需要特殊說明的是,以上每一個棧幀(大括號括起來的),最上面(也就是地址遞增方向)的都是幀指針位置,最下面的都是棧指針位置。然而寄存器中只有%ebp和%esp保存棧幀指針,因此同一時間只能保存一對。當進展到第三層的時候,已經有了三個棧幀(原則上來講一定是多于3個),寄存器當然是存不下的,因此就需要在存儲器當中備份一下,之后再恢復。于是就出現了每個棧幀的幀指針指向的存儲器位置,都會備份著外層方法(也就是調用者)的幀指針。

  當方法遞歸到n=1結束時,棧幀會自下向上依次收回,棧幀指針(也就是%ebp和%esp當中的值)都會依次向上移動,直到程序結束。也就是說,上面的三幅圖,如果倒過來,就是遞歸方法依次結束時棧幀的狀態。

  由此就可以看出,過程當中棧幀建立以及完成的慣例,可以保證遞歸調用的正常運行,包括循環調用。不得不說,這群計算機界的大神們實在是太牛了,盡管當棧幀出現以后,看起來也并不復雜,但難點就在于無中生有的發現或者說某種意義上的創造。

?

作者:zuoxiaolong(左瀟龍)

出處:博客園左瀟龍的技術博客--http://www.cnblogs.com/zuoxiaolong

轉載于:https://www.cnblogs.com/zzdbullet/p/9629909.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/537873.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/537873.shtml
英文地址,請注明出處:http://en.pswp.cn/news/537873.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

php 相親 段子,精彩的男女幽默段子

精彩的男女幽默段子。撒嬌老婆洗完澡對老公撒嬌說&#xff1a;老公&#xff0c;抱我到床上去吧。老公看了看老婆&#xff0c;冷冷的回答道&#xff1a;我還是把床搬過來吧&#xff01;所以&#xff0c;撒嬌還是要看體型&#xff01;單身老公說&#xff1a;老婆&#xff0c;你不…

Redmine數據庫備份及搬家

Bitnami Redmine的備份分2種方式&#xff1a; 1.導出數據庫 2.整個目錄搬家 不管是哪種都想停掉服務&#xff0c;redmine相關的服務有以下5個&#xff1a; redmineApache   redmineMySQL   redmineSubversion   redmineThin1   redmineThin2 可以打開windows服務控制面…

且看BCH開啟的“信用本位”時代

??? 且看BCH開啟的“信用本位”時代 比特幣向來被稱為“金本位”的互聯網實驗&#xff0c;由于中本聰先生的天才發明&#xff0c;POW機制給予了比特幣與黃金同樣的生產模式。所以&#xff0c;時至今日&#xff0c;BCE依然自稱為“數字黃金”。 只可惜&#xff0c;“一葉障目…

oracle設置臨時表空間,Oracle臨時表空間查看、添加臨時表空間數據文件、修改默認臨時表空間 方法!...

--查表空間使用率情況(含臨時表空間)SELECT d.tablespace_name "Name", d.status "Status",TO_CHAR (NVL (a.BYTES / 1024 / 1024, 0), 99,999,990.90) "Size (M)",TO_CHAR (NVL (a.BYTES - NVL (f.BYTES, 0), 0) / 1024 / 1024,99999999.99) US…

Redmine項目管理工具安裝

Redmine免費開源的項目管理工具 下載 一鍵安裝工具 https://bitnami.com/stack/redmine/installer 安裝 Redmine一鍵安裝工具集成了php服務&#xff0c;mysql服務。盡管安裝就好。 安裝完成后&#xff0c;在開始菜單&#xff0c;找到-----Bitnami Redmine Stack--------Bi…

Oracle創建假脫機文件,oracle – 在sqlplus中假脫機csv文件時的標頭格式

我需要使用sqlplus從Oracle中的表中調整csv.以下是所需的格式&#xff1a;"HOST_SITE_TX_ID","SITE_ID","SITETX_TX_ID","SITETX_HELP_ID""664436565","16","2195301","0""664700792&qu…

方便微信公眾號等手機網頁調試插件eruda和vConsole

原文地址&#xff1a;https://blog.csdn.net/qq_39234840/article/details/80951710 ---------------------------------------------------------- 調試插件一&#xff1a;eruda&#xff08;推薦&#xff0c;因為比vConsole功能多&#xff09; <script src"//cdn.js…

HDU 3530Subsequence(單調隊列)

題意 題目鏈接 給出$n$個數&#xff0c;找出最長的區間&#xff0c;使得區間中最大數$-$最小數 $> m$ 且$< k$ Sol 考慮維護兩個單調隊列。 一個維護$1 - i$的最大值&#xff0c;一個維護$1 - i$的最小值。 至于兩個限制條件。 $<k$可以通過調整隊首來滿足 $>a$可以…

oracle權限培訓,Java培訓-ORACLE數據庫學習【2】用戶權限

查詢用戶擁有的權限&#xff1a;1.查看所有用戶&#xff1a;select *from dba_users;select *from all_users;select *from user_users; 2.查看用戶或角色系統權限(直接賦值給用戶或角色的系統權限)&#xff1a;select *from dba_sys_privs;select *from user_sys_privs; 3.查看…

linux 中文件夾的文件按照時間倒序或者升序排列

1&#xff0c;按照時間升序 命令:ls -lrt 詳細解釋: -l use a long listing format 以長列表方式顯示&#xff08;詳細信息方式&#xff09; -t sort by modification time 按修改時間排序&#xff08;最新的在最前面&#xff09; -r reverse order while sorti…

PHP中關于時間(戳)、時區、本地時間、UTC時間等的梳理

PHP中關于時間&#xff08;戳&#xff09;、時區、本地時間、UTC時間等的梳理 在PHP開發中&#xff0c;我們經常會在時間問題上被搞糊涂&#xff0c;比如我們希望顯示一個北京時間&#xff0c;但是當我們使用date函數進行輸出時&#xff0c;卻發現少了8個小時。幾乎所有的php猿…

WebServiceStudio.exe測試webservice接口工具

WebServiceStudio.exe測試webservice接口工具 下載鏈接 https://pan.baidu.com/s/1gf8ajS3 打開工具WebServiceStudio&#xff0c;如下填寫地址&#xff0c;點擊【Get】按鈕 會顯示出需要傳參的地方&#xff0c;在value中填寫xml參數 輸入完value值后&#xff0c;點擊【Invok…

oracle最大實例數,【ORA-16196】一個實例在其生命周期里最多只能裝載和打開一個數據庫...

如果使用“alter database open;”命令打開一個曾經被“alter database close;”命令關閉的數據庫時&#xff0c;您將會收到如下的報錯信息&#xff1a;"ORA-16196: database has been previously opened and closed"這個報錯的原因是什么呢&#xff1f;原因是&#…

Navicat工具導出Mysql數據表結構到Excel文件中

原文鏈接&#xff1a;https://blog.csdn.net/zt15732625878/article/details/77978266 ------------------------------------------------------------------------ 前言 項目中數據庫設計已經完成&#xff0c;現在到了代碼實現的階段&#xff0c;數據庫中沒有數據&#xff…

利用MAVEN的profile 實現打包環境的切換

樂哉碼農產生問題的背景 由于在項目開發的時候&#xff0c;我們一般都是使用的本地庫&#xff0c;數據庫連接寫的是本地的&#xff0c;如果我們將項目打成war的時候&#xff0c;里面的配置連接寫的是我們本地的&#xff0c;當我們直接把war拷貝到服務器上面進行部署的時候&…

服務器oracle優化,oracle服務器配置及優化

1.在ORACLE中實現分布式快速存取和充實內存是很重要的。要不惜任何代價避免頁面調度和交換﹐每次都必須把系統全局區(SGA)放到內存。將SGA放到內存中﹐在INIT.ORA中設置參數 PRE_PAGE_SGAPRE_PAGE_SGAYES2.回卷段的竟爭會降低系統的性能。SELECT GETS,WAITS from V$ROLLSTAT;…

Android 常用的數據加密方式

前言 Android 很多場合需要使用到數據加密&#xff0c;比如&#xff1a;本地登錄密碼加密&#xff0c;網絡傳輸數據加密&#xff0c;等。在android 中一般的加密方式有如下&#xff1a; 亦或加密AES加密RSA非對稱加密當然還有其他的方式&#xff0c;這里暫且介紹以上三種加密算…

oracle可以注入嗎,ORACLE 注入

1判斷是什么數據庫and exist(select * from dual)and exists(select * from user_tables)原理&#xff1a;dual表和user_tables表是oracle中的系統表返回正常&#xff0c;那么就可以肯定這是oracle。2查字段數order by 10-- //錯誤,列數小于10order by 3-- //正常,列數等于…

centos升級glibc(升級到 2.17版)

1、原先的系統glibc庫的版本是2.12&#xff0c;需要升級到2.17版本。 下載地址&#xff1a; http://ftp.gnu.org/gnu/glibc/ http://ftp.gnu.org/gnu/glibc/glibc-2.17.tar.gz 這里可以選擇你所需要的版本。 2、安裝部署 [rootkafzook1 common]# tar -xf glibc-2.17.tar.g…

Day31 python基礎--網絡編程基礎-socketserver

一&#xff0c;驗證客戶端合法性 #server端 import os import hmac import socket secret_key balex_sbdef auth(conn):msg os.urandom(32) #生成一個隨機的字符串conn.send(msg) #發送到client端result hmac.new(secret_key,msg) #處理這個隨機字符串&#xff0c;得到一…