www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當(dāng)前位置:首頁 > 公眾號精選 > IOT物聯(lián)網(wǎng)小鎮(zhèn)
[導(dǎo)讀]示例代碼說明執(zhí)行主程序初始狀態(tài)執(zhí)行代碼前5句準(zhǔn)備調(diào)用子程序調(diào)用子程序子程序寄存器入棧保護(hù)計算字符串長度返回結(jié)果寄存器出棧返回指令ret在任何一門編譯型語言中,棧操作都是非常重要的。利用棧的后進(jìn)先出特性,可以很方便的解決一些棘手的問題,以至于CPU單獨分配了push和pop這兩個命...

  • 示例代碼說明


  • 執(zhí)行主程序


    • 初始狀態(tài)


    • 執(zhí)行代碼前 5 句


    • 準(zhǔn)備調(diào)用子程序


    • 調(diào)用子程序


  • 子程序


    • 寄存器入棧保護(hù)


    • 計算字符串長度


    • 返回結(jié)果


    • 寄存器出棧


    • 返回指令ret


在任何一門編譯型語言中,棧操作都是非常重要的。


利用棧的后進(jìn)先出特性,可以很方便的解決一些棘手的問題,以至于CPU單獨分配了push和pop這兩個命令來專門操作棧,當(dāng)然了,還有其他一些輔助的棧操作指令。


對于一些解釋型的腳本語言,比如:Javascript、Lua等,它們與宿主語言之間的參數(shù)傳遞也都是通過來操作的。


因此,理解了棧操作的基本原理,對于學(xué)習(xí)、理解高級語言是非常有幫助的。


這篇文章,我們繼續(xù)從最底層的指令碼入手,通過一個子程序調(diào)用(即:函數(shù)調(diào)用),來學(xué)習(xí)棧空間是如何操作的,也就是下面這張圖:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里示例雖然是匯編代碼,但是指令碼一共不超過10個,而且每一句都有注釋,相信你閱讀一定沒有問題!


再次重申:我們不是在學(xué)習(xí)匯編語言,只是利用匯編代碼,去繁存簡,用最簡單的實例來理解棧的操作。


示例代碼說明

Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里


代碼的功能是:


主程序:設(shè)置數(shù)據(jù)段、棧段、棧頂這 3 個寄存器,然后調(diào)用子程序(函數(shù)調(diào)用);


子程序:從寄存器 si 中獲取字符串開始地址,然后計算字符串的長度,最后通過寄存器 ax 返回給主程序;


主程序在調(diào)用子程序的時候,就涉及到返回地址的入棧、出棧操作。


子程序在計算字符串長度的時候,為了保護(hù)一些使用到的寄存器不被破壞,也涉及到入棧和出棧操作。


我們的主要目標(biāo)就是來研究以上這2部分操作時,棧空間里的數(shù)據(jù)變化情況。



執(zhí)行主程序

以下演示的截圖,是通過debug.exe這個工具來調(diào)試的。


在調(diào)試的過程中,主要關(guān)心的就是??臻g中的數(shù)據(jù),以及幾個寄存器的值:


代碼相關(guān):cs, ip
棧相關(guān):ss, sp


初始狀態(tài)

在執(zhí)行第一條指令之前,首先看一下所有寄存器中的值:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里此時,我們還沒有為數(shù)據(jù)段寄存器ds、棧段寄存器ss賦值,因此里面的值是沒有意義的。


只有cs:ip寄存器的值是有意義的,此時它們?yōu)?76F:0000,指向第一條代碼處


再來看一下指令碼:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里兩個綠框內(nèi)的指令,就是用來設(shè)置數(shù)據(jù)段寄存器ds、棧段寄存器ss和 棧頂寄存器sp。


這部分內(nèi)容在上一篇文章中都已經(jīng)詳細(xì)描述過了,這里就不重復(fù)了。


執(zhí)行代碼前 5 句

mov ax, data
mov ds, ax

mov ax, stack
mov ss, ax
mov sp, 20h
這5行代碼的功能就是:設(shè)置ds、ss和sp。


執(zhí)行完這5行代碼后,寄存器中的值為:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里從以上這張圖中可以看到編譯器為程序安排了下面這幾個地址:


把【數(shù)據(jù)段】安排在 076C:0000 位置;
把【棧段】  安排在 076D:0000 位置;
把【代碼段】安排在 076F:0000 的位置;


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里雖然數(shù)據(jù)段值定義了6個字節(jié)的數(shù)據(jù)(5個字符1個結(jié)束符),但是它與棧段的開始地址之間,預(yù)留了16個字節(jié)的空間。


我們把此時內(nèi)存空間的整體布局畫一下:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里

準(zhǔn)備調(diào)用子程序

我們都知道,在調(diào)用函數(shù)的之后,需要把調(diào)用指令后面的那條指令的地址,壓入到棧中。


只有這樣,被調(diào)用函數(shù)在執(zhí)行結(jié)束之后,才能繼續(xù)返回到正確的指令處繼續(xù)執(zhí)行。


CPU在執(zhí)行call指令的時候,會自動把call指令的后面一條指令的地址,壓入到棧中。


在執(zhí)行call指令之前,我們先來看一下2張圖片。


(1)call 的指令碼和匯編代碼
Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里call的匯編代碼是:call 0018。


0018指的是指令寄存器ip的值,加上代碼段寄存器cs,就是:076F:0018,這個位置處存儲的就是子程序的第一條指令:push bx。


注意:call的指令碼是E80500,E8是call指令的操作碼,0005是指令參數(shù)(注意:低字節(jié)是放在低地址,即:小端模式)。


之前文章說過,CPU在執(zhí)行一條指令后,會自動把指令寄存器ip修改為下一條指令的地址。


當(dāng)call這條指令執(zhí)行時,ip就自動變成下一條指令的地址,再加上call指令中的0005,也就是說讓ip再加上這個值,就是子程序的第一條指令的地址。


這也是相對地址的概念!在以后介紹到重定位的時候,再繼續(xù)聊這個話題。


(2) ??臻g的數(shù)據(jù)
Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里此時,棧頂寄存器sp的值為0020,即:棧的最高地址的下一個位置(為什么是這個位置?上一篇文章有說明)。


這32個字節(jié)的內(nèi)容是沒有任何意義的。


因為棧里數(shù)據(jù)是否有意義,是依賴于sp寄存器的,可以把它理解成一個指針,有些書籍中稱呼它為:棧頂指針。


調(diào)用子程序

子程序的功能是計算字符串的長度,那么主程序一定要告訴子程序:字符串的開始地址在哪里


在代碼的開頭,我們放置了6個字節(jié)的數(shù)據(jù)段空間,內(nèi)容是5個字符,加上一個 0。


主程序把第一個字符的地址 0,通過寄存器si來告訴子程序:mov si, 0。


子程序在執(zhí)行時,就從si的值所代表的地址處,依次取出每一個字符。


現(xiàn)在我們開始執(zhí)行call指令。


從上面的描述中可以知道:call的下一條指令的地址(076F:0013),將會被壓入到棧中。


由于這里call指令是段內(nèi)跳轉(zhuǎn),不會把cs的值入棧,僅僅是把ip的值入棧。(如果是段間跳轉(zhuǎn)的話,就會把cs:ip都壓棧)


我們來看一下執(zhí)行call指令之后的兩張圖:


(1) 寄存器的值
Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里從圖中看出sp的值變成了001E。還記得之前文章說的入棧操作嗎?


Step1:sp = sp -2。由于 sp 的初值是 0020,減去 2 之后就是 001E(都是十六進(jìn)制);


Step2:把要入棧的值(也就是下一條指令的地址 0013)放在 sp 指向的地址處。


從圖中還可以看到,指令寄存器ip的值變成了0018,也就是子程序的第1條指令(push bx)的地址。


(2)棧空間的數(shù)據(jù)
Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里可以看到:最后 2 個字節(jié)是0013,也即是下面的這樣:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里此時,指令寄存器ip指向了子程序的第一條指令 076F:0018 處,那就繼續(xù)執(zhí)行吧!


子程序

保護(hù)使用到的寄存器

我們知道:CPU中寄存器都是公用的。


在子程序中,為了計算字符串的長度,代碼中用到了bx,cx這 2 個寄存器。


但是我們不知道這 2 個寄存器是否在主程序中也被使用了。


如果我們冒然直接使用它們,改變了它們的值,那么在子程序執(zhí)行結(jié)束后,返回到主程序時,主程序如果也用了這 2 個寄存器,那就有麻煩了。


因此,在子程序的開始處,需要把bx,cx放在在棧中進(jìn)行暫存保護(hù)。


當(dāng)子程序返回的時候,再從棧中恢復(fù)它們的值,這樣就不會對主程序構(gòu)成潛在的威脅了。


1. push bx
在入棧之前,bx的值是0000,我們給他入棧。


還記得上篇文章中入棧的操作嗎:


Step1: 把 sp 的值減 2;
Step2: 把要入棧的值放在 sp 地址處(2個字節(jié));


此時,棧頂寄存器sp變成 001C (001E - 2)


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里再來看一下棧空間的數(shù)據(jù)情況:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里此刻,棧中有意義的數(shù)據(jù)就有2個:返回地址,bx 的值


2. push cx
在入棧之前,cx的值是005C,我們給他入棧。


執(zhí)行入棧的2步操作之后,棧頂寄存器sp變成 001A (001C - 2)


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里棧空間的數(shù)據(jù)情況:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里
3. 計算字符串的長度
字符串是放在數(shù)據(jù)段中的。數(shù)據(jù)段的段地址ds,在主程序的開頭已經(jīng)設(shè)置好了。


字符串的首地址,主程序在執(zhí)行call指令之前,已經(jīng)放在寄存器si中了。


因此,子程序只要從si開始位置,依次取出每一個字符,然后檢查它是否等于 0 (jcxz)。


如果不為0,就把長度值加 1 (inc bx),然后繼續(xù)取下一個字符(inc si);


如果為0,就停止獲取字符,因為已經(jīng)遇到了字符串末尾的 0。


在循環(huán)獲取每一個字符的時候,可以用bx寄存器來記錄長度,所以在子程序的開頭要讓bx入棧。


讀取的每個字符,放在cx寄存器中,所以在子程序的開頭要讓cx入棧。


我們來看一下檢查第一個字符 'a' 的情況:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里此時:


bx的值為0001,說明長度至少為1。


si的值為0001,準(zhǔn)備取下一個位置ds:si(即:076C:0001)處的字符 ‘b’


這個過程一直循環(huán)6次(loop s),當(dāng)ds:si指向076C:0005,也就是取出的字符為0時,就直接跳轉(zhuǎn)到標(biāo)號為over(即:076F:0027)的地址處。


此刻,寄存器bx中就存放著字符串的長度:0005:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里
4. 把字符串長度告訴主程序
字符串的長度計算出來了,我們要把這個值告訴主程序,一般都是通過通用寄存器ax來傳遞返回結(jié)果。


所以,執(zhí)行指令 mov ax, bx 把bx的值賦值給ax,主程序就可以從寄存器ax中得到字符串的長度了。


5. pop cx
子程序在返回之前,需要把棧中保存的bx、cx值恢復(fù)到寄存器中。


另外,由于棧的后進(jìn)先出特性,需要把棧頂數(shù)據(jù)先彈出到cx寄存器中。


在執(zhí)行出棧之前:


sp = 001A
cx = 0000


棧中的數(shù)據(jù)情況如下:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里pop cx指令分為2個動作:


Step1:把 sp 指向的地址單元的中數(shù)據(jù)( 2 個字節(jié)),放入寄存器 cx 中,于是 cx 中的值變成了:005C;


Step2:把 sp 的值自增 2,變成 001C (001A 2)。


此時,棧中的數(shù)據(jù)情況:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里
6. pop bx
執(zhí)行過程是一樣的:


Step1:把 sp 指向的地址單元的中數(shù)據(jù)( 2 個字節(jié)),放入寄存器 bx 中,于是 bx 中的值變成了:0000;


Step2:把 sp 的值自增 2,變成 001E (001C 2)。


此時,棧中的數(shù)據(jù)情況:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里
7. 返回指令 ret
CPU在執(zhí)行ret指令時,也有2個動作:


Step1:把 sp 指向的地址單元的中數(shù)據(jù)( 2 個字節(jié)),放入指令寄存器 ip 中,于是 ip 中的值變成了:0013;


Step2:把 sp 的值自增 2,變成 0020 (001E 2)。


此時,棧中的數(shù)據(jù)情況是:


Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里Linux從頭學(xué)04:所有編程語言中的棧操作,底層原理都在這里這時,棧頂寄存器sp已經(jīng)指到了代碼段的空間中。這是由于我們在剛開始安排的時候,沒有在棧與代碼之間,空出來一段緩沖空間。


不管怎樣,此時:


??臻g中沒有任何有意義的數(shù)據(jù)了;
cs:ip 指向了主程序中 call 指令的下一條指令(mov ax,4c00h);


所以,當(dāng)CPU執(zhí)行下一條指令的時候,又回到了主程序中繼續(xù)執(zhí)行。。。



------ End ------
本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
關(guān)閉
關(guān)閉