手撕彙編。。。

彙編系列文章已經更新了三篇,每一篇都是筆者用心總結,希望對你有幫助

手把手教你彙編 Debug

愛了愛了,這篇寄存器講的有點意思

之前的文章我們主要聊了一些基本的彙編指令,並且通過一個名為 Debug 的調試軟件,讓我們看到了內存中是如何存儲指令和數據的,在學習了這些之後,我們就可以了解彙編程序了。

程序的執行過程

首先通過一個示意圖給大家介紹一下程序的執行過程,我們以 C 語言一個簡單的 hello.c 程序為例。

image-20200324063226578

這就是一個完整的 hello world 程序執行過程,會涉及幾個核心組件:預處理器、編譯器、彙編器、連接器,下面我們逐個擊破。

  • 預處理階段(Preprocessing phase),預處理器會根據開始的 # 字符,修改源 C 程序。#include <stdio.h> 命令就會告訴預處理器去讀系統頭文件 stdio.h 中的內容,並把它插入到程序作為文本。
  • 然後是 編譯階段(Compilation phase),編譯器會把文本文件 hello.i 翻譯成文本hello.s,它包括一段彙編語言程序(assembly-language program)。

彙編語言是非常有用的,因為它能夠針對不同高級語言來提供自己的一套標準輸出語言。

  • 編譯完成之後是彙編階段(Assembly phase),這一步彙編器會把 hello.s 翻譯成機器指令,把這些指令打包成可重定位的二進制程序(relocatable object program) 放在 hello.c 文件中。
  • 最後一個是鏈接階段(Linking phase),這個階段就是用鏈接器把翻譯過後的程序合併在一起,生成在操作系統上直接運行的可執行文件的過程。

所以,一般來說,可執行文件包括兩個方面

  1. 程序和數據,這些是構成可執行文件的基本信息。
  2. 相關的描述信息,比如空間多大,程序有多大等,這些是構成可執行文件的必要因素。

認識彙編程序

同樣的,先上一則彙編代碼,然後下面再慢慢概述。

assume cs:code
code segment
		mov ax,1234H
		add ax,ax
		mov bx,1111H
		add bx,bx
code ends
end

這段彙編代碼有幾個地方你可能不太了解,不過 mov、add 指令你應該知道是什麼意思(如果你看完筆者之前文章並進行了仔細研究的話)。

構成彙編程序的指令分為兩種:一種是彙編指令,一種是偽指令,彙編指令就是我們上面提到的 mov 、add 指令,這些指令有實際的意義,比如 mov 就是移動寄存器或者數據,add 就是對寄存器或者數據進行加法操作。而且 mov 和 add 這類彙編指令在內存中有對應的機器碼存在,最終會有 CPU 執行。而偽指令沒有實際的意義,它們指令簡單的定義一個程序段,這些偽指令會由編譯器來直接解釋,它們在內存中沒有對應的機器碼,所以不會由 CPU 來執行。

上面提到的偽指令有三種,即

code segment
	......
code ends

segment 和 ends 是一組成對出現的指令,而且這一對指令必須成對出現,缺了誰都不行。這一對指令定義了一個段,segment 標識着段的開始,ends 標識着段的結束。code 表示段的名稱,段名稱可以隨意替換。

彙編程序由多個段組成(至少包含一個段),這些段被用來存放代碼、數據或者當做棧空間來使用。上面例子代碼中的段由代碼組成,所以叫代碼段。

除了段之外,彙編程序還需要有 assume ,這同樣是一條偽指令,它的意思是假設,它假設某一段寄存器和某個段相關聯,通過 assume 來說明這種關聯關係。assume 不用深入理解,我們只要知道編程時將特定用途的段和相關寄存器關聯起來即可。

end 是一段彙編程序結束的標誌,它也是一條偽指令,編譯器在編譯彙編程序的過程中,遇到 end 就會停止編譯,所以,如果我們彙編程序寫完了,就需要在程序的末尾加上 end ,表示程序的結束。

在彙編程序中,除了彙編指令和偽指令,還有一種標號,比如上面代碼中的 code,標號位於 segment 的前面,作為段的名稱,這個段的名稱最終將被編譯、連接處理為一個段的段地址。

再次提醒下,注意這裡不要搞混了 end 和 ends ,ends 是和 segment 一起使用的表示彙編段,而 end 是彙編結束的標識。

所以總結下,用彙編語言編寫的源程序,包括偽指令和彙編指令,偽指令是由編譯器來執行,彙編指令可以翻譯成機器代碼並最終由 CPU 執行。

以後,我們可以將源程序文件中的內容稱為源程序,將源程序中最終由計算機執行、處理的指令或數據,稱為程序。程序最先以彙編指令的形式存在於原程序中,然後經過編譯、連接後轉變為機器碼,存儲在可執行文件中,如下圖所示

image-20211203203151861

所以,總結一點來說,編寫一個彙編程序主要分為下面這幾步

  • 首先定義一個段 ,比如 code、abc 等
  • 在段中寫入彙編指令
  • 指出程序在何時處結束
  • 標號要和寄存器關聯起來。
  • 程序返回(後面要說)

程序返回

一個完整的程序是要有返回條件的,程序只有在執行完相關代碼後,執行返回條件,讓出 CPU 執行權,操作系統才會分配時間片給其他程序,程序不能一直霸佔着 CPU 不放,這是一種資源的浪費,而且一直佔用着 CPU,也會導致程序崩潰。

彙編語言中,實現程序返回的指令只有兩行

mov ax,4c00H
int 21H

解釋下這兩句指令的意思:

mov ax,4c00H 就是把 4c00 移動到 ax,中,INT 21H 是調用系統中斷指令,這兩句代碼起作用的就是 AH = 4CH,意思就是調用 INT 21H 的 4CH 號中斷,該中斷就是安全退出程序。

到目前為止,我們已經了解到了幾種和結束的相關內容,比如段結束,彙編程序結束、還有我們剛剛說的程序返回,下表列出了這三個指令的區別。

image-20211207203007204

程序錯誤

一般來說,彙編語言的程序錯誤分為兩種:即語法錯誤邏輯錯誤

語法錯誤很簡單,說白了就是你彙編語言指令寫錯了,這個程序編譯時期就能夠發現。

邏輯錯誤是在運行時發生的,一般不容易被發現,排查起來比較困難,比如下面這段代碼不寫程序返回就是屬於邏輯錯誤。

assume cs:code
code segment
		mov ax,1234H
		add ax,ax
		mov bx,1111H
		add bx,bx
code ends
end

為什麼?因為你這段代碼沒有加程序返回邏輯。類似的這種邏輯錯誤還有很多,這些錯誤需要在具體的場景中才能發現。

編寫彙編

下面我們開始用編輯器來編寫彙編源程序,只要將彙編存儲為文本文件,再經過編譯器編輯,CPU 運行即可。

我們可以使用多種文本格式來編寫彙編程序,比如我們可以使用最簡單的文本文件來編寫(基於 win7 操作系統環境)

assume cs:codeseg
codeseg segment
		mov ax,0123H
		mov bx,0456H
		add ax,bx
		add ax,ax
codeseg ends
end

編寫完成後,存儲為 .asm 後綴文件,這是一種彙編格式。

編譯

一個完整的彙編程序執行流程分為編寫、編譯、鏈接和運行,所以接下來我們需要對編寫完成的彙編程序進行編譯。在編譯之前我們需要找到一個相應的編譯器,這裡我們採用的是 masm 5.0 彙編編譯器,執行程序是 masm.exe

(為了防止大家再從網站上亂找資源,我下載下來放在了網盤中,大家在程序員cxuan 後台回復 masm 即可領取)

說到使用 masm 5.0 的這個過程我踩了很多坑,這裡給大家提示下,及時閉坑!!!

  • masm 5.0 是穩定版本,網絡上流傳的 6.x 不知道怎麼樣,我是沒運行成功。
  • masm 5.0 要在 win7 環境下運行,我使用 win11 測試,程序不兼容,不知道其他版本如何。win7 版本可以正常運行。

安裝完成後,我們打開 cmd ,進入下載並解壓好的 masm 5.0 文件夾下。

然後直接鍵入 masm

image-20211206222357207

運行 masm 後,首先會顯示一些版本信息,然後輸入需要被編譯的原程序文件名稱,這裡需要注意一下,[.ASM]提示我們,默認的文件擴展名是 asm,比如我們要編譯的源程序文件名是 test.asm,這裡直接輸入 asm 即可。如果源程序文件不是以 .asm 為後綴,需要輸入它的全名,也就是 test.txt。

這裡我們輸入的是 test,因為我們編寫的文件是 .asm 後綴。

image-20211206222527589

輸入源程序文件名後,按 enter 鍵,程序會提示我們輸入要編譯出的目標文件名稱,目標文件名稱是我們對源程序進行編譯後的最終結果。Object filename 的後綴名是 .obj,因為 .asm 文件會自動編譯為 .obj 文件,所以我們不必再指定文件名,直接按 enter 鍵,會直接生成 .obj 文件。

確定了目標文件名稱後,會出現 Source listing ,這是提示我們要輸入列表文件的名稱,這個文件是編譯器將源程序編譯為目標文件的過程中產生的中間結果,可以讓編譯器不生成這個文件,直接鍵入 enter 即可。如果編譯器要生成這個文件,它的後綴名是 .lst

然後繼續提示出 Cross-reference ,這是提示我們要輸入交叉引用文件名稱,這個文件和 Source listing 一樣,是編譯器產生的中間結果,可以不讓編譯器生成這個文件,我們直接按 enter 即可。如果編譯器要生成這個文件,它的後綴名是 .crf

最後編譯器會進行一個結果輸出,這個輸出結果會顯示警告錯誤和必須要改正的錯誤,可以從上圖中看出來,我們程序沒有警告和編譯錯誤。

在輸入源程序文件名的時候要指出所在路徑,如果遇到 unable to open input file 這個問題,最好把彙編程序直接放在 C 盤,我放在桌面上,也就是 C:\Users\Administrator\Desktop 下,也會出現此錯誤。

連接

在對源程序編譯後得到目標文件後,我們需要對目標文件進行連接,從而得到可執行文件。上一步我們得到了 .obj文件,現在我們需要將 .obj 文件連接成為 .exe 也就是可執行文件。

為了實現我們的需求,我們需要藉助微軟的 Overlay Linker 3.60 連接器,文件名為 link.exe,這個應用程序不用再次下載(在我公眾號回復拿到的軟件會包括編譯器和連接器,解壓後,它們都會在 masm 文件夾下)。

現在我們進入 DOS,cd 到 masm 文件中,鍵入 link

image-20211207192942324

運行 link 後,會出現一些版本信息,然後提示需要被連接的目標文件名稱,這裡仍需要注意,默認文件是 .obj 結尾,所以如果你需要連接的文件是 obj 文件,就不用輸入後綴名,如果不是 obj 文件,則需要輸入全名。

我們剛剛編譯了一個 test.obj 文件,所以我們直接對這個 obj 文件進行連接。

輸入要連接的文件名(這裡仍需要輸入 obj 所在的路徑),按 enter 。

image-20211207193412012

輸入 enter 後,會繼續來一個三連提示。

第一個提示表明程序繼續提示我們輸入要生成可執行文件的名稱,可執行文件是我們對一個程序進行連接要得到的最終結果,默認的 .exe 文件是 TEST.EXE ,所以我們不再需要指定文件名。這裡也可以指定生成可執行文件所在的目錄,我們也不需要,繼續向下走。

第二個提示是連接程序提示輸入映像文件的名稱,這個文件是連接程序將目標文件連接為可執行文件過程中的中間結果,也可以讓連接程序不生成這個文件,繼續向下走。

第三個提示是連接程序提示輸入庫文件的名稱,庫文件包含了一些可以調用的子程序,如果程序調用了庫中的子程序,就需要指定,否則不需要。

最後會出現一個 waring: no stack segment,我曾一直以為出現這個提示就不會再生成最終執行文件,但是當我仔細檢查之後我才發現這只是一個 waning ,最終的執行文件在 masm 文件夾下,我截個圖給你看。

image-20211207194410472

這個提示只是告訴我們沒有棧段,我們可以完全忽略這個提示,當然如果你的程序有問題,是無法生成連接之後的文件的。

連接這個過程很有用,歸結來說,主要有三個作用

  • 當源程序很大時,可以將它分為多個源程序文件來進行編譯,每個單獨編譯之後的目標文件,可以再通過連接將它們連接到一起生成可執行文件。
  • 程序中調用了某個庫文件中的子程序,需要將這個庫文件和目標文件連接到一起生成一個可執行文件。
  • 在編譯過後生成的機器碼文件,其中有些內容還不能直接執行,連接程序需要將這些內容轉換為可執行信息,才能夠把編譯過後的機器碼文件,連接成為可執行文件。

執行應用程序

現在我左手一個 asm 文件,右手一個 obj 文件,嘴裏叼着一個 exe 文件,所以我就是嘴遁王者。廢了半天勁,終於將 asm 搞成 exe 文件了,累屁了,不過先別急着休息,還差最後一步,執行它!

於是我們執行以下 TEST.EXE 文件

image-20211207195435408

我有點蒙,這怎麼啥都沒有啊,輸出結果呢?。。。。。。

細想了一下,哦,我們沒有用任何庫來向控制台輸出信息,我們只是做了一些數據和寄存器的移動、相加操作。

當然我們可以向控制台輸出信息,不過這個我們後面在演示。

簡單聊聊程序的裝載過程

我們大家知道,一個程序如果要執行,就需要裝載進入內存,然後 CPU 從內存中取指執行命令。

那麼,當我們使用 DOS 的時候,誰負責將可執行程序裝載進入內存的呢?

在 DOS 中,有一個叫做命令解釋器 command.com 這個玩意兒,它也是 DOS 系統的 shell。

DOS 啟動後,會先進行初始化,然後運行 command.com ,command.com 運行後,執行完其他相關任務後,會在屏幕上顯示提示符,等待用戶輸入。

如果用戶輸入要執行的命令,比如 cd ,taskkill 等,這些命令由 command 執行,執行完成後再次等待用戶輸入。

如果用戶輸入要執行的程序,command 會通過文件名找到可執行文件,然後將它載入內存,設置 CS:IP 執行入口,然後 command 暫停運行,CPU 執行程序,程序執行完成後,返回 command ,command 再次等待用戶輸入。

所以,一個完整的彙編程序的執行過程如下。

image-20211207202534720

如果這篇文章寫的不錯並且對你有一些幫助,那我就求個贊呀!