經(jīng)過前幾天的基礎(chǔ)知識(shí)學(xué)習(xí),我們已經(jīng)對(duì)匯編指令有了一個(gè)宏觀上的了解,我們學(xué)習(xí)了硬件,又學(xué)習(xí)了一些常見的指令,可是這些都無法真正的被叫做一個(gè)程序。不過不要緊,學(xué)習(xí)完今天的內(nèi)容,我們就可以真正的做出我們的第一個(gè)程序。 一、一個(gè)程序的誕生過程不知道小伙伴們平時(shí)有沒有想過,我們平時(shí)經(jīng)常使用的一些應(yīng)用軟件到底是如何被制作出來的?如何做到我們只需要點(diǎn)擊一個(gè)桌面的EXE文件就可以運(yùn)行?我們是如果做到將一些只有機(jī)器認(rèn)識(shí)的指令轉(zhuǎn)化為大家都能夠使用的軟件呢?如果按照一個(gè)程序的誕生過程來講,首先第一個(gè)步驟肯定是軟件工程師們將想實(shí)現(xiàn)的匯編指令編寫成一個(gè)指令文件。這個(gè)文件的編寫只需要借助我們常用的文本編輯器(比如Edit,記事本,notepad++等等),而我們生成的文本文件的作用主要就是記錄我們想要運(yùn)行的匯編指令。接下來的第二個(gè)步驟,我要做的就是想辦法讓計(jì)算機(jī)可以認(rèn)識(shí)這些匯編指令。這個(gè)時(shí)候我們就要使用匯編語言編譯程序?qū)υ闯绦蛭募械脑闯绦蜻M(jìn)行編譯,產(chǎn)生計(jì)算機(jī)可以識(shí)別的目標(biāo)文件,接著我們還需要將目標(biāo)文件關(guān)聯(lián)起來,保證計(jì)算機(jī)系統(tǒng)可以正確的運(yùn)行這些文件。第三步也就是最后我們只需要執(zhí)行可執(zhí)行文件中的程序即可。
如果小伙伴對(duì)這些文字概念不太理解,沒關(guān)系,接下來就讓我們一步一步的來創(chuàng)建我們第一個(gè)可執(zhí)行的源程序! 1. 源程序還記得我們之前講過的匯編指令嗎?
這些語句其實(shí)都有一個(gè)統(tǒng)一的分類:指令語句。什么是指令語句?是指可以被翻譯成機(jī)器指令直接被CPU識(shí)別的指令代碼,在編譯時(shí)操作系統(tǒng)會(huì)為這些語句分配存儲(chǔ)單元幫助運(yùn)行,這種指令每一條都代表著計(jì)算機(jī)所具有的一個(gè)基本能力,因此這些指令又被叫做可執(zhí)行語句。與之相對(duì)的,還有一部分我們之前沒有接觸過的語句是不能夠被計(jì)算機(jī)直接識(shí)別的,它們只在編譯階段發(fā)揮作用,需要有編譯器(MASM,TASM等)來翻譯,其主要作用就是知識(shí)匯編程序應(yīng)該如何識(shí)別匯編程序,因此這種語句又被叫做命令語句或偽指令語句。
為了保證我們今天可以寫出我們的第一個(gè)程序,需要先學(xué)習(xí)幾個(gè)保證程序運(yùn)行的偽指令。 1.1 偽指令(1)segmentsegment再英語中有片段的意思,見名知意的角度來看,segment主要是輔助我們聲明一個(gè)指令段。segment的出現(xiàn)一定會(huì)伴隨另一個(gè)偽指令ends一起出現(xiàn),segment聲明一個(gè)段的開始,ends標(biāo)示著一個(gè)段的結(jié)束。 segment和ends的使用格式: 段名:segment 代碼段 XXX 段名 ends (2)endend和ends不同,這個(gè)小伙伴們需要注意,end主要是用來標(biāo)識(shí)一個(gè)匯編程序的結(jié)束。編譯器再編譯匯編程序的過程中,如果碰到了偽指令end,就會(huì)結(jié)束對(duì)匯編程序的編譯,因此我們?cè)诰帉懗绦虻臅r(shí)候,如果程序?qū)懲炅?,就需要在結(jié)尾處加上偽指令end,否則編譯器無法感知編譯工作已經(jīng)結(jié)束。 (3)assumeassume再英文中有假設(shè)的含義。 在匯編中,它假設(shè)某一段寄存器和程序中的某一個(gè)用segment...ends定義的段相互關(guān)聯(lián),通過assume偽指令說明這種關(guān)聯(lián)。在需要的情況下,編譯程序可以將段寄存器和某一個(gè)具體的段相互聯(lián)系。assume并不是一條非要深入理解的偽指令,我們只需要在編程時(shí),記著用assume將有特定用途的段和相關(guān)的段寄存器關(guān)聯(lián)起來即可。 1.2 源程序與實(shí)際運(yùn)行的'程序'我們通常說的源程序是指用匯編語言中的匯編指令和偽指令編寫的文件,而我們編程的目的是為了讓計(jì)算機(jī)完成我們?cè)O(shè)定的特定任務(wù)。源程序中我們寫入的匯編指令組成了最終計(jì)算機(jī)執(zhí)行的程序,而相對(duì)的偽指令則是交由編譯器處理,他們并不會(huì)參與我們特定目的實(shí)現(xiàn)的組成,所以我們常說的源程序中的'程序'實(shí)際上是指最終被計(jì)算機(jī)識(shí)別,運(yùn)行的指令和數(shù)據(jù)。 1.3 標(biāo)號(hào)除了匯編指令和偽指令外,我們還會(huì)在一段源程序中發(fā)現(xiàn)一些標(biāo)號(hào),比如卸載segment前的代碼段名稱就屬于標(biāo)號(hào)的一種,它最終會(huì)被編譯,連接程序處理為一個(gè)段的段地址。
1.4 程序的結(jié)構(gòu)在此之前,我們?cè)趯W(xué)習(xí)指令語句的時(shí)候都是通過DOSBOS的Debug功能進(jìn)行的,這樣對(duì)于簡短的程序確實(shí)是十分的方便,但是如果對(duì)于語句很多,邏輯很復(fù)雜的程序,顯然就不能夠再采取這種方式,我們需要編寫一個(gè)用來編譯的源程序,而相對(duì)的,這個(gè)源程序也需要有它自己的結(jié)構(gòu)。 我們通過一個(gè)簡單的任務(wù)實(shí)驗(yàn)來講解 任務(wù): 求2的三次方是多少
首先我們需要聲明一個(gè)可以用來運(yùn)算的代碼段 codeA segment ... 此處代表省略的代碼段邏輯 codeA ends
接著,我們還要為代碼段中填入具體的匯編邏輯 codeA segment mov ax,2 add ax,ax add ax,ax codeA ends
然后我們指出程序需要在哪里結(jié)束 codeA segment mov ax,2 add ax,ax add ax,ax codeA ends end
當(dāng)然codeA我們假定他是個(gè)代碼段,我們需要指定一個(gè)段地址寄存器與它進(jìn)行關(guān)聯(lián)(當(dāng)然也并非一定要) assume cs:codeA codeA segment mov ax,2 add ax,ax add ax,ax codeA ends end
1.5 程序返回當(dāng)一個(gè)程序執(zhí)行結(jié)束后,我們需要通知CPU并且交還它的控制權(quán),那么我們?cè)撊绾螌?shí)現(xiàn)呢?現(xiàn)階段我們接觸到的概念有三種方式與結(jié)束有關(guān),分別是: 1.段名 ends 它標(biāo)識(shí)了一個(gè)指令段的結(jié)束2.end 寫在程序結(jié)尾,標(biāo)識(shí)一個(gè)程序的結(jié)束3.mov ax,4c00 int 21 :表示一個(gè)程序終端,程序返回(了解即可,現(xiàn)階段無需深究) 1.6 語法錯(cuò)誤和邏輯錯(cuò)誤還記得剛剛我們計(jì)算2的三次冪的程序嗎: assume cs:codeA codeA segment mov ax,2 add ax,ax add ax,ax codeA ends end 它在運(yùn)行時(shí)其實(shí)會(huì)遇到一些問題,因?yàn)檫@個(gè)程序并沒有設(shè)定一個(gè)返回值,當(dāng)然這個(gè)問題再編譯的時(shí)候是沒有辦法被發(fā)現(xiàn)的,我們沒有辦法說這段源碼是一個(gè)錯(cuò)誤的程序。因?yàn)樗恼Z法是沒有問題的(這里其實(shí)還有一個(gè)隱藏的問題,稍后會(huì)提到),僅是邏輯上存在一些漏洞。而相對(duì)應(yīng)的,如果我再該段代碼中拼錯(cuò)單詞,比如assume codeA修改為codeB,由于程序中并沒有codeB的聲明,因此在編譯階段就會(huì)被編譯器發(fā)現(xiàn),這種就是很明顯的語法錯(cuò)誤。 2. 創(chuàng)建源程序的過程當(dāng)我們了解完一個(gè)源程序的相關(guān)結(jié)構(gòu)后,我們可以嘗試的創(chuàng)建一個(gè)源程序出來。 2.1 編輯源程序首先我們需要有個(gè)容器來保存我們的源程序,當(dāng)然不需要很復(fù)雜,我們只需要?jiǎng)?chuàng)建一個(gè)純文本文檔即可。 然后我們將剛剛的程序粘進(jìn)去(這里我們需要加上返回語句)
2.2 編譯源程序(1) 編譯器MASM當(dāng)我們完成源文件的編寫后,我們接下來就需要將源文件編譯成一個(gè)包含機(jī)器指令的目標(biāo)文件。
這里講一下編譯器應(yīng)該怎么用:由于masm再win10環(huán)境下不兼容,因此需要搭配DosBox一起使用,在使用前我們先講下關(guān)于DosBox的配置文件,讓我們打開DosBox: 當(dāng)我們運(yùn)行DosBox的時(shí)候,我們可以看到兩個(gè)窗口,其中下面這個(gè)窗口上有描述配置文件的位置 我們按照路徑找到配置文件: 打開配置文件,然后添加兩行配置命令 mount d: d:\Code\ASM d: 我們來解釋下這兩句話是什么含義: 1.mount d: d:\Code\ASM :這里面的mount命令是DosBox的內(nèi)置命令,是為我們掛載默認(rèn)的工作盤符,如果不做指定,我們就需要每次進(jìn)入DosBox的時(shí)候都輸入一次。這里的d:\Code\ASM是我新建的一個(gè)文件夾,這個(gè)文件夾地址小伙伴們可以自己選擇,創(chuàng)建完文件夾后需要將剛剛下載的文件夾中的內(nèi)容復(fù)制進(jìn)去; 2. 第二行的d: 如果我們掛載了多個(gè)工作空間,就需要通過我們?cè)O(shè)定的標(biāo)識(shí)符進(jìn)入不同的工作空間,這里的d就是在我們啟動(dòng)DosBox時(shí)默認(rèn)進(jìn)入剛剛配置的工作空間。 配置完masm編譯器我們就可以在dosbox中使用了,具體的使用方法如下: 打開dosbox,輸入masm進(jìn)入編譯器 這里編譯器提示我們輸入源文件地址,我們需要將文件的全路徑輸入進(jìn)去(小技巧,我們可以將文件放入我們剛剛掛載的工作空間內(nèi),就可以直接輸入文件名): 接下來編譯器要求我們輸入的時(shí)編譯后目標(biāo)文件的名稱,如果不想改名字,直接回車即可 確認(rèn)了生成的文件名后,編譯器接下來詢問我們編譯目標(biāo)文件時(shí)是否產(chǎn)生列表文件,這里直接回車即可 忽略了列表文件生成后,這里編譯器詢問我們是否產(chǎn)生交叉引用文件,忽略即可 好了,到這里編譯已經(jīng)完成了,我們發(fā)現(xiàn)出現(xiàn)了一個(gè)語法錯(cuò)誤,錯(cuò)誤信息為error A2107:Non-digit in number。這里解釋下,這個(gè)錯(cuò)誤是由于我們?cè)诰帉憛R編文件時(shí),并沒有指定數(shù)字的進(jìn)制,我們需要為我們的文件指定對(duì)應(yīng)的進(jìn)制,修改后如下:
修改后我們?cè)龠\(yùn)行一次: 此時(shí)已經(jīng)運(yùn)行成功了,進(jìn)入對(duì)應(yīng)的工作空間會(huì)發(fā)現(xiàn)新生成了一個(gè)OBJ后綴的文件: 2.3 連接剛剛我們完成了codeA.asm到codeA.OBJ的轉(zhuǎn)變,接下來我們需要將文件進(jìn)行連接,從而得到一個(gè)可以直接被執(zhí)行的.exe文件。
(1) 打開dosbox,輸入link命令 這里我們輸入想要連接的文件名 接著輸入我們想要生成的目標(biāo)文件名,采用默認(rèn)的話回車即可: 這里是詢問我們是否生成映射文件,現(xiàn)階段忽略即可: 同樣,這里是詢問是否生成庫文件,忽略即可。 到這里連接就結(jié)束了,我們還是查看工作空間,會(huì)發(fā)現(xiàn)一個(gè)新生成的EXE文件。
連接的作用這里我們簡單的講一下連接的作用: 1.當(dāng)源程序很大時(shí),可以拆分為多個(gè)源程序文件編譯,每個(gè)源程序文件編譯成目標(biāo)文件后,可以通過連接程序?qū)⑺麄冞B接在一起,生成一個(gè)可執(zhí)行文件。2.程序中調(diào)用了某個(gè)庫文件中的子程序,需要將這個(gè)庫文件和該程序生成的目標(biāo)文件連接到一起,生成一個(gè)可執(zhí)行文件。3.一個(gè)源程序編譯后,得到了存有機(jī)器指令的目標(biāo)文件,目標(biāo)文件中的有些信息還不能直接生成可執(zhí)行文件,需要連接程序?qū)⑦@些信息處理為最終可運(yùn)行的內(nèi)容,所以再只有一個(gè)源程序文件而又不需要調(diào)用其他庫的時(shí)候,也必須進(jìn)行連接操作。 2.4 簡化編譯、連接過程當(dāng)然MASM和LINK也為我們提供了簡化的方式: 總結(jié)一下格式就是: masm或link命令 + 文件盤符+分號(hào)。 2.5 exe可執(zhí)行文件這里我們已經(jīng)生成了學(xué)習(xí)到這里的第一個(gè)可執(zhí)行文件:codeA.exe,很遺憾win10的操作系統(tǒng)還是無法直接運(yùn)行它,我們依然要借助DosBox運(yùn)行: 是不是很奇怪,為什么程序運(yùn)行后沒有任何結(jié)果,仿佛沒有運(yùn)行一樣?其實(shí)程序肯定是運(yùn)行了的,只不過我們的程序中并沒有任何向顯示器輸出信息的指令,不過不要遺憾,隨著課程的進(jìn)行,我們會(huì)學(xué)到更多的指令來幫助我們完成各種各樣復(fù)雜的程序。 二、補(bǔ)充1. exe文件與內(nèi)存在我們調(diào)用可執(zhí)行文件的時(shí)候,操作系統(tǒng)會(huì)為EXE文件本身劃分一段安全的內(nèi)存,這段內(nèi)存僅供當(dāng)前的exe文件使用。而在DOS中,可執(zhí)行文件若想要執(zhí)行,必須有一個(gè)正在運(yùn)行的程序?qū)⑵浼虞d入內(nèi)存,在上面我們運(yùn)行codeA.exe的時(shí)候,就是由DosBox程序?qū)⑵浼虞d入內(nèi)存,其運(yùn)行結(jié)束后再將CPU的控制權(quán)交還。 2. 程序執(zhí)行過程的追蹤當(dāng)然我們也可以通過debug程序配合運(yùn)行我們的exe文件: 然后我們就可以通過Debug自身的命令來觀察CPU各個(gè)寄存器的變化: 這里我們看到我們編寫在源程序中的指令已經(jīng)被加載進(jìn)來了,接下來使用T命令執(zhí)行即可,這里我就不再贅述了,感興趣的可以查看《匯編語言 第四版》中相關(guān)描述: 3. psp內(nèi)存區(qū)1.當(dāng)我們啟動(dòng)dosbox的時(shí)候,操作系統(tǒng)會(huì)為它規(guī)劃一段起始地址為SA:0000(即起始地址偏移量為0)的容量足夠的內(nèi)存區(qū)。2.這段內(nèi)存區(qū)的前256個(gè)字節(jié)中,會(huì)創(chuàng)建一個(gè)稱為程序前綴(PSP)的數(shù)據(jù)區(qū),DOS系統(tǒng)需要利用PSP來和被加載進(jìn)來的可執(zhí)行程序進(jìn)行通信。3.從PSP內(nèi)存區(qū)后面,程序?qū)?huì)被裝入,起始地址為:SA+10H:0000 ;4.將該內(nèi)存區(qū)的段地址存入ds寄存器中,初始化其他相關(guān)寄存器后,將cs:ip指向程序的入口。 三、預(yù)習(xí)實(shí)驗(yàn)這里為了接下來的學(xué)習(xí),需要同學(xué)們將課本中的實(shí)驗(yàn)三完成: 結(jié)語 |
|