這是 x86 匯編連載系列第七篇文章,前六篇文章見文末。
上回我們簡(jiǎn)單認(rèn)識(shí)了一下什么是段,段前綴和一段安全的段空間是哪里,但是程序中不會(huì)僅有一個(gè)段,復(fù)雜程序必然是包含多個(gè)段的,這篇文章我們就來了解下多個(gè)段的相關(guān)程序。
內(nèi)存地址空間是由操作系統(tǒng)直接管理和分配的,一般操作系統(tǒng)分配空間有兩種方式:
程序由磁盤載入內(nèi)存中時(shí),會(huì)由操作系統(tǒng)直接為程序分配其運(yùn)行所需要的內(nèi)存空間。
程序在運(yùn)行時(shí)可以動(dòng)態(tài)向操作系統(tǒng)申請(qǐng)分配內(nèi)存空間。
如果想要在程序被載入時(shí)獲得內(nèi)存空間分配,我們就需要在源程序中對(duì)其進(jìn)行聲明,通過定義多個(gè)段的方式來申請(qǐng)內(nèi)存空間。這樣的好處是能夠保證段內(nèi)的數(shù)據(jù)連續(xù),而且對(duì)于我們來說也能夠清晰明白的看懂程序邏輯,所以我們一般采用定義多個(gè)段的方式編寫程序。
在代碼段 cs 中使用數(shù)據(jù)
現(xiàn)在考慮這樣一個(gè)問題,如何累加下面這幾個(gè)數(shù)據(jù)的和,并把它放在一個(gè) ax 寄存器中呢?
0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
我們當(dāng)然可以通過一個(gè)數(shù)據(jù)接一個(gè)數(shù)據(jù)這樣進(jìn)行累加,并把每次累加的結(jié)果都用 ax 進(jìn)行存儲(chǔ),簡(jiǎn)單一點(diǎn)的方式是通過循環(huán)的方式累加,一共需要累加 8 次數(shù)據(jù),用 ax 存儲(chǔ)。但是現(xiàn)在就有一個(gè)問題,這 8 個(gè)數(shù)據(jù)應(yīng)該放在哪呢?我們之前學(xué)的累加做法都是把他們放在一組連續(xù)的內(nèi)存單元,這樣我們就可以累加內(nèi)存中的數(shù)據(jù)來把它們進(jìn)行累加了,但是如何把這些數(shù)據(jù)放在一個(gè)連續(xù)的內(nèi)容單元中呢?這段內(nèi)存單元又是從哪找呢?
我們可以不用自己找內(nèi)存單元,直接讓操作系統(tǒng)分配,我們只需要定義這些數(shù)據(jù)并把它們放在一個(gè)連續(xù)的內(nèi)存單元即可,具體該怎么做呢?請(qǐng)看下面代碼
assume cs:code code segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h mov ax,0 mov bx,0 mov cx,8 s: add ax,cs:[bx] add bx,2 loop s mov ax,4c00h int 21h code ends end
上面匯編代碼中出現(xiàn)了之前我們沒有學(xué)過的 dw,dw 的含義是定義字型數(shù)據(jù),dw 的全稱就是 define word,上面代碼使用 dw 定義了 8 個(gè)字型數(shù)據(jù),它們所占用的空間為 16 個(gè)字節(jié)。
定義了數(shù)據(jù)之后,我們就需要對(duì)這 8 個(gè)數(shù)據(jù)進(jìn)行累加,我們?cè)撊绾握业竭@ 8 個(gè)數(shù)據(jù)呢?
仔細(xì)觀察上面代碼,我們可以知道這 8 個(gè)數(shù)據(jù)在代碼段 cs 中,所以我們可以通過 cs 來找到這幾個(gè)數(shù)據(jù),所以 cs 是段地址,而偏移地址是用 ip 表示的,也就是說這 8 個(gè)數(shù)據(jù)的偏移地址分別是 cs:0 cs:2 cs:4 cs:6 cs:8 cs:a cs:c cs:e 。
我們將上面這段代碼編寫、編譯和鏈接后,對(duì)其 exe 文件進(jìn)行 debug :
??????等下,這個(gè) and ax,[bx+di] 是什么東西?再看下 cs:ip 的地址,是初始地址沒錯(cuò)啊,為啥沒看到程序中的指令呢?我們?cè)儆?debug -u 看看
這前幾條指令都是什么東西?怎么 mov bx,0000 在偏移地址 0013 處?
從上圖中我們可以看到,程序被加載入內(nèi)存之后,所占內(nèi)存空間的前 16 個(gè)單元存放在源程序中用 dw 定義的數(shù)據(jù),后面的單元存放源程序中匯編指令所對(duì)應(yīng)的機(jī)器指令。
那么如何執(zhí)行匯編指令所定義的機(jī)器指令呢?你可以直接 -t 慢慢的執(zhí)行到 mov bx,0 處,也可以改變 ip 寄存器的值,也就是 ip = 10h,從而使 cs:ip 指向程序的第一條指令。
這兩種方式看起來哪個(gè)都有一定的局限性,那么還有沒有更簡(jiǎn)潔一點(diǎn)的方式呢?
看下面這段代碼
assume cs:code code segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h start: mov ax,0 mov bx,0 mov cx,8 s: add ax,cs:[bx] add bx,2 loop s mov ax,4c00h int 21h code ends end start
仔細(xì)看這段代碼,和上面那段代碼有什么區(qū)別?
只有兩個(gè)區(qū)別,一是在 mov ax,0 前面加了一個(gè) start: 標(biāo)志,并且在 end 后加了一個(gè) start 標(biāo)志,這兩個(gè)標(biāo)志分別指向程序的開始處和結(jié)束處。
那么問題來了,為什么你加一個(gè) start: 標(biāo)志就說這是程序的開始處,我隨便加個(gè)比如 begin: ,能不能成為程序的開始處呢?
assume cs:code code segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h begin: ...... code ends end begin
經(jīng)過我的實(shí)驗(yàn)驗(yàn)證是可以的,為什么呢?
關(guān)鍵點(diǎn)并不在于你定義的是什么標(biāo)志,而在于最后的 end,end 除了能夠告訴編譯器程序結(jié)束的位置外,還能夠告知編譯器程序是從哪里開始的,在上面代碼中我們用 start: 和 end start 告訴程序從 start 處開始,并執(zhí)行到 end start 處結(jié)束,而 mov ax,0 是程序的第一條指令。
程序開始標(biāo)志 start: 和 IP
我們知道,CS 和 IP 這兩個(gè)寄存器能夠指明程序的開始處和程序執(zhí)行的偏移地址,而且 start: 標(biāo)號(hào)指明了程序開始的地方,那么 start: 標(biāo)號(hào)所指向的偏移地址是不是我們之前討論的 10h 處呢?
我們編譯鏈接執(zhí)行程序看一下。
我們通過 -u 和 -r 分別執(zhí)行了一下,可以看到,程序的起始地址都是 076A:0010 ,這也就是說,除了我們 dw 定義的幾條數(shù)據(jù)外,IP 指向的偏移地址 0010 就是程序的開始處。
所以有了這種方法,我們就可以這樣安排程序框架:
assume cs:code code segment ...數(shù)據(jù) start: ... 代碼 code ends end start
在代碼段 cs 中使用棧
除了在 cs 代碼段中定義數(shù)據(jù),還可以在 cs 代碼段中定義棧,比如下面這個(gè)需求:
利用棧將程序定義的數(shù)據(jù)逆序存放,源代碼如下
assume cs:codesg codesg segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ...... codesg ends end
如何實(shí)現(xiàn)將數(shù)據(jù)逆序存放呢?可以這么思考:
先定義一段和數(shù)據(jù)相同大小的棧空間,然后把數(shù)據(jù)入棧,然后再依次出棧就能夠?qū)崿F(xiàn)逆序存放了,因?yàn)闂J呛笕胂瘸龅臄?shù)據(jù)結(jié)構(gòu),最開始 push 進(jìn)去的最后 pop 。
代碼如下:
assume cs:codesg codesg segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 start:mov ax,cs mov ss,ax mov sp,30h mov bx,0 mov cx,8 s0:push cs:[bx] add bx,2 loop s mov bx,0 mov cx,8 s1:pop cs:[bx] add bx,2 loop s1 mov ax,4c00h int 21h codesg ends end start
解釋下上面這段代碼:
首先先定義了 16 個(gè)值為 0 的字型數(shù)據(jù),程序被載入內(nèi)存后,操作系統(tǒng)會(huì)為程序分配 16 個(gè)字型數(shù)據(jù)空間,用于存放 16 個(gè)數(shù)據(jù),把這段空間當(dāng)做棧來使用。
start 標(biāo)號(hào)處開始程序,首先先要設(shè)置一段空間為棧段,由于我們只有一個(gè) cs 段,所以程序是從 cs 段開始的,剛開始 dw 的 8 個(gè)數(shù)據(jù)所需的地址空間為 cs:0 ~ cs:f,后面 dw 定義的 16 個(gè)字所需的地址空間是 cs:10 ~ cs:2f ,由于 ss:sp 這兩個(gè)寄存器始終指向棧頂,目前還沒有數(shù)據(jù)入棧,所以棧頂必須為 2f + 1 ,也就是 30h 才可。
編譯鏈接執(zhí)行后的棧段如下圖所示
這是程序被載入后的內(nèi)存分配情況,可以看到,cs:0 ~ cs:f 存儲(chǔ)的是 8 個(gè)字型數(shù)據(jù),cs:10 ~ cs:2f 存儲(chǔ)的是 16 個(gè)字型 0 數(shù)據(jù)。
程序執(zhí)行完成后的內(nèi)存分配圖如下,可以看到已經(jīng)實(shí)現(xiàn)了數(shù)據(jù)的逆序存放。
可見,我們定義這些數(shù)據(jù)的最終目的,是通過它們?nèi)〉靡欢ㄈ萘康膬?nèi)存空間,所以我們?cè)诿枋?dw 的作用時(shí),可以說用它來定義數(shù)據(jù),也可以說用它來開辟一段內(nèi)存空間。比如上面的 dw 0123h ... 0987h ,可以說定義了 8 個(gè)字型數(shù)據(jù),也可以說開辟了 8 個(gè)內(nèi)存空間,它們的效果是一樣的。
多個(gè)段的使用
上面討論的內(nèi)容都是將數(shù)據(jù)和棧放入一個(gè)段中,這樣做雖然比較省事,但是程序邏輯不夠清晰,像個(gè)大雜燴一樣,而且都放入一個(gè)段中,這個(gè)段的內(nèi)存有可能不夠用(8086 CPU 中一個(gè)段最大不能超過 64 KB)。
為了能夠清晰說明程序邏輯,并且能夠容納數(shù)據(jù)和棧,我們一般使用多個(gè)段的方式分別將數(shù)據(jù)、代碼和棧分別放入各自的段中。
比如我們通過多個(gè)段的方式將上面實(shí)現(xiàn)逆序存放的程序進(jìn)行改寫,代碼如下
assume cs:codesg,ds:data,ss:stack data segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h data ends stack segment dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 stack ends code segment start: mov ax,stack mov ss,ax mov sp,20h mov ax,data mov ds,ax mov bx,0 mov cx,8 s0: push [bx] add bx,2 loop s0 mov bx,0 mov cx,8 s1: pop [bx] add bx,2 loop s1 mov ax,4c00h int 21h code ends end start
如上代碼所示,在 assume 后面定義了三個(gè)段,這些都是偽指令,只是為了方便說明段的類型。然后分別在 data segment 和 stack segment 定義了相關(guān)數(shù)據(jù),最后再 code segment 編寫的程序代碼。
基本上代碼和在一個(gè)段中的定義是一樣的,值得說明是,同一個(gè)段中的 ss:sp 指向的是 ss:30 ,而在這段代碼中的 ss:sp 指向的是 ss:20 ,這個(gè)大家知道是怎么回事吧?由于數(shù)據(jù)會(huì)直接定義在 data segment 中,所以棧段的 16 個(gè)字型數(shù)據(jù)占用的空間就是 ss:0 ~ ss:1f,所以 ss:sp 指向 20h 處。
其實(shí),代碼段、數(shù)據(jù)段、棧段完全是我們自己定義的,但是并不是我們分別定義了 cs:code,ds:data,ss,stack 之后,程序就會(huì)把它們分別當(dāng)做程序段、數(shù)據(jù)段和棧段的。要知道這些和 assume 一樣都是偽指令,它們是由編譯器執(zhí)行的,CPU 并不知道這些指令的存在,所以我們必須要在程序中告訴 CPU 哪個(gè)是棧段、哪個(gè)是數(shù)據(jù)段。
該如何告訴 CPU 呢?
我們看下棧段的設(shè)置指令
mov ax,stack mov ss,ax mov sp,20h
這樣就會(huì)將 ss 指向 stack ,ss:sp 用來指向 stack 棧段的棧頂?shù)刂罚挥性谶@個(gè)指令執(zhí)行后,CPU 才會(huì)把 stack 段當(dāng)做棧段來使用。
相同的,CPU 如果想訪問 data 段中的數(shù)據(jù),則可用 ds 指向 data 段,用其他寄存器比如 bx 來存放 data 段中的偏移地址即可。
所以,我們完全可以按照下面這種方式來定義程序
assume cs:a,ds:b,ss:c b segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h b ends c segment dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 c ends a segment d: mov ax,c mov ss,ax mov sp,20h mov ax,b mov ds,ax mov bx,0 mov cx,8 s0: push [bx] add bx,2 loop s0 mov bx,0 mov cx,8 s1: pop [bx] add bx,2 loop s1 mov ax,4c00h int 21h code a end d
最終程序的功能和上面的一模一樣。
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7232瀏覽量
90708 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
3100瀏覽量
74859 -
操作系統(tǒng)
+關(guān)注
關(guān)注
37文章
7019瀏覽量
124660 -
程序
+關(guān)注
關(guān)注
117文章
3816瀏覽量
82116 -
代碼
+關(guān)注
關(guān)注
30文章
4876瀏覽量
69956
原文標(biāo)題:多個(gè)段的程序
文章出處:【微信號(hào):cxuangoodjob,微信公眾號(hào):程序員cxuan】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
一文全面了解linux相關(guān)知識(shí)
一文了解STM32啟動(dòng)過程

一文詳解PLC子程序與子程序指令

評(píng)論