Hello大家好,我是程序員cxuan!這篇文章我們來進行實際操作一下Debug。
我們以后將會用到很多 Debug 命令,這里我們先來熟悉一下它們。
Debug 是 Windows / Dos 操作系統提供的一種功能。使用 Debug 能讓我們方便查看 CPU 各種寄存器的值、內存情況,方便我們調試指令、跟蹤程序的運行過程。
接下來我們會用到很多 debug 命令,但是使用這些命令的前提是,你需要在電腦上安裝一下 debug,Windows/Mac 都可以安裝,獲取鏈接我已經給你找出來了。阿,忘記說了,我們這里使用的是 Dos box來模擬匯編的操作環境。
傳送門(Mac 和 Windows 都是):https://www.dosbox.com/download.php?main=1
下載完成后打開 DosBox ,打開之后是這樣的。
此時我們輸入 debug 命令應該提示的是
因為我們還沒有進行連接和掛載,此時我們執行
mount c D:\debug
執行這條命令時,你需要現在 D 盤下創建一個 debug 文件夾,然后我們掛載到 debug 下面。
并且執行 C: 切換到 C 盤路徑下。
此時我們就可以執行 debug 命令了。
這里需要注意一點,我在 Windows 10 系統下搭建 Debug 環境時,在掛載完成后輸入 debug ,還是提示 Illegal command:debug ,此時你需要再下載一個 debug.exe ,貼心的我也把下載地址給你了。
下載地址:https://pan.baidu.com/s/177arSA34plWqV-iyffWpEw#list/path=%2F 密碼:3akd
需要下載里面的 debug.exe,然后把它放在你掛載的路徑下,這里我掛載的路徑時 D 盤下的 debug 文件夾。
放置完成之后,再輸入 debug 就可以了。
因為每次打開 Dosbox 都會執行上面這些命令,真的好煩,那怎么辦呢?一個簡單的辦法是在 Dosbox 安裝路徑下找到
打開之后,在末尾鍵入
就 OK 了,下次直接打開 Dosbox ,會默認執行這三條命令,至此,就是我搭建 Dosbox 遇到的所有問題了。
玩兒匯編得學會用 Debug ,Debug 是一種調試程序,通過 Debug 能讓我們能夠看到內存值,跟蹤堆棧情況,看到寄存器所暫存的內容等,同時也能夠更好地幫助我們理解匯編代碼,所以學會 Debug ,非常重要,這是一種不可或缺的動手能力。
下面我們會用到幾種 Debug 命令,這里先簡單介紹下。
Debug 命令有很多,不過常用的一般就上面這幾個。
好了,現在我們直接進入正題,開始在 Dosbox 上正式進行 Debug 操作,首先打開 Dosbox。
嗯。。。。。。這個界面我們打開很多次了。
那我寫個命令呢?好吧,沒演示過,下面就來了!
親,用 Debug -r 就可以查看和修改 CPU 寄存器內容了呢。
查看寄存器內容。
這里需要注意一下 -r 大小寫的問題,Debug -r 是查看寄存器內容。而 -R 則是無效指令。
上圖列出來了很多寄存器,你可能覺得無從下手,不要亂,我們先從最基本的開始入手,也就是 CS 和 IP,CS(Code Segment)是代碼段寄存器,一般也被稱為段基址,可以認為是程序訪問的入口,CPU 需要從 CS 中找到從哪個位置開始取指執行,但是我們還不知道要取哪一段,這時候 IP 的作用就體現出來了,IP(Instruction Pointer)就是指令指針寄存器,也叫做偏移地址,它會告訴我們從段基址開始,取哪一段的地址。
可以使用段基址:偏移地址來確定內存中的指定地址。
這里我們只是簡單聊一下這兩個寄存器的概念,要了解這兩個寄存器的具體作用,可以看筆者的上一篇文章
使用 -r 也能夠修改寄存器的內容,如下所示
-r 一般的格式是 -r 寄存器,然后系統會進行冒號提示,后面就是你要修改的內容。
使用 -d 指令可以查看內存中的內容。
輸出的內存值默認是按照 CS:IP 的地址開始的,由于 CS 的值默認是 073F,而 IP 默認是 0100,所以 -d 的內存值是 073F:0100 。
-d 的格式很多,下面只介紹一下常用的幾種格式。
形似 -d 1000:0 這種 -d 段基址 偏移地址的格式可以產生如下輸出。
如上圖所示,Debug 會列出指定內存單元中的的內容。上圖中的每一個 00 都表示 8 位,如果是 4A,那么這八位展開來說就是 0010 1011 。每一行有 16 個 8 位,所以構成了 128 位內存地址。
為什么都是 00 呢,因為內存單元的值沒有被改寫,說白了就是這塊內存區域沒有存值,如何改寫我們后面會說。
每一行的中間都有一個 -,這個是為了便于我們閱讀來設置的,- 號前后都有 8 個內存單元,這樣便于查看。
右側幾個 ...... 表示每個內存單元可顯示的 ASCII 碼字符,因為內存沒有值,所以也沒有對應的 ASCII 碼。我們可以數一下,每行有 16 個 . ,這表示每一個 00 都對應了一個 ASCII 碼。
我們可以使用 -d 1000:9 這種 -d 段基址:起始偏移地址 格式來顯示從 1000 的第幾位開始。
Debug 從 1000:9 開始,一直到 1000:88,一共是 128 個字節,第一行中的 1000:0 ~ 1000:8 中的內容沒有顯示。
還可以使用 -d 1000:0 9 這種 -d 段基址:起始偏移地址 結尾偏移地址的格式來輸出。
還可以是使用 -d 偏移地址來在不指定段基址的情況下,查看內存值。
上面說的都是查看內存中指定位置或者區域的值,下面我們要來改寫一下內存值。
使用 -e 可以改寫內存值,比如我們想要改寫 1000:0 ~ 1000:f 中的內容,可以使用 -e 1000:0 0 1 2 3 4 5 6 7 8 9 0 a b c d e f 這種方式,如下圖所示。
這里需要注意下,在進行 -e 改寫的時候,每個值中間都有一個空格,如果沒有空格的話,會當做一個內存值來看待。
然后用 -d 1000:0 看到我們剛改寫的內存值。
還可以使用提問的方式來逐個修改從某一地址開始的內存單元的內容。
還是用 1000:100 來舉例子,輸出 -e 1000:100 后按下回車鍵。
如上圖所示,可以看到我們先輸入了一次 -e 1000:100 這個指令,然后按下了回車鍵。
注意,如果這里你按下了回車鍵,就相當于整個 -e 改寫的過程已經完成。
如果你想要繼續改寫后面內存中的值,你需要按下空格鍵。
我們改寫了 1000:100 之后的內存值,然后使用 -d 1000:100 查看我們改寫的內容是否生效。
-e 命令還可以支持寫入字符,比如我們可以向 1000:0 這個位置開始寫入數值和字符,-e 1000:0 1 'a' 2 'b' e 'c' 。
如上圖所示,當我們向內存寫入字符 'a' 'b' 'c' 的時候,會自動轉換為 ASCII 碼進行存儲,在最右側可以找到剛剛寫入的字符。
如何向內存中寫入一段機器碼呢?比如我們想要在內存中寫入一段機器碼。
我們可以使用 -e 來進行寫入,向內存中寫入 b8 01 00 b9 02 00 01 c8 這個機器碼,如下所示
我們使用 -e 寫入之后,使用 -d 查看內存值,可以發現我們剛剛寫入的值,但是卻看不到機器碼,所以機器碼該如何看呢?
別急,還有個 -u 命令,這個就是看機器碼的,如下圖所示,我們使用 -u 命令顯示我們寫入的機器碼。
可以看到 1000:0000 ~ 1000:0006 這個內存地址使我們寫入的機器碼,-u 這個命令就是將內存單元的內容翻譯為匯編指令并顯示。
-u 輸出的結果分為三部分顯示:
1000:0 處存放的是寫入的機器碼 B8 01 00 組成的機器指令,對應的匯編指令是 MOV AX,0001。
1000:0003 處存放的是寫入的機器碼 B9 02 00 組成的機器指令,對應的匯編指令是 MOV CX,0002。
1000:0006 處存放的是寫入的機器碼 C1 C8 所組成的機器指令,對應的匯編指令是 add ax,cx。
上面介紹的一系列指令包括我們上面提到的 Debug -e 機器碼都是向內存中進行寫入,那么如何執行這些指令呢?
我們可以使用 Debug -t 來執行寫入的指令。使用 Debug -t 可以執行由 CS:IP 指向的指令。
既然是 -t 能夠執行從 CS:IP 指向的命令,所以我們有必要將 CS:IP 指向 1000:0(因為我們前面將指令寫在了 1000:0 處)。
首先我們需要執行 -r cs 1000 ,-r ip 0 把 CS:IP 賦值為 1000:0。
然后執行 -t 指令,下圖是已經執行過的指令截圖。
可以看到,執行完 -t 指令之后,MOV AX,0001 這條指令被執行,當前 AX 寄存器的內容變為了 0001,這條匯編指令的意思就是把 0001 移動到 AX 寄存器中。
繼續執行 -t 之后,我們可以看到寄存器的變化。
畢竟機器指令不是那么好懂,寫入很不方便,所以有沒有辦法能夠支持我們直接寫入匯編指令呢?還真有,Debug 提供了 -a 這種方式來實現匯編指令的寫入。如下圖所示
可以看到,我們使用了 -a 命令來對 1000:0 進行寫入,分別輸入 mov ax,1 mov bx,2 mov cx,3 add ax,bx add ax,cx add ax,ax 指令,然后按回車進行確定執行。
我們使用 -d 1000:0 f 可以看到從偏移地址 0 處開始的第 f 個內存指令(因為最大寫入的地址只是 f)。
上圖中的 1000:000F 為什么有值呢,因為我們上面已經執行過這個寫入了。
另外,使用 -a 可以從一個預設的地址處開始輸入指令。
今天和大家聊了一下 Debug 的基本用法,主要包括
匯編指令的選項有很多,上面介紹的這些屬于經常用到的指令,這些指令要能夠熟練使用。
以下三幅圖片為磁盤結構的簡圖:
磁盤的中間有一個主軸,它可以帶動盤片轉動,讓所有的盤片都圍繞著主軸旋轉。有些磁盤的盤片上會有兩個盤面,即上下兩個盤面。
盤面被劃分成許多個狹窄的同心圓環,每一個同心圓環都被稱為一個磁道,磁道的編號是由外向內的,即最外圈的為0號磁道。 將盤面看成一個圓形,可以在盤面上劃分出多個相同的扇形,分配到每一個磁道上的弧段被稱為扇區。可以看出每一個磁道上的扇區數是相同的。注意上圖右半邊的注解。 每一個磁道上的扇區數目相同,且每個扇區的大小都為512字節,但外圈的扇區占用的物理面積更大,因此外圈的磁道上的扇區存在更多的浪費。
盤片旁邊為機械磁臂,用于移動磁頭。上圖中用藍色標出的圓柱體為一個柱面,例如所有盤面的0磁道可以組成一個柱面。只要往磁盤控制器中寫柱面(cylinder)、磁頭(head)、扇區(sector),就可以使用磁盤了。
扇區號的排列會影響磁盤的訪問速度,Linux0.11使用的磁盤的扇區號排列方式如下:
從圖中可以看出:扇區號是從一個柱面最上面的一個磁道從上往下開始排列,若一個磁道上有7個扇區,最上面磁道的扇區排列為0,1,2,3,4,5,6;7號扇區在下面一個磁道,且位于0號扇區的正下方。下一個柱面的扇區號從上一個柱面結束的扇區號+1開始排列,即扇區號的排列是連續的。這樣數據的讀寫就變為了按柱面進行的,而不是按盤面進行。
磁盤容量 = 盤面數 × 柱面數 × 扇區數 × 512字節; //柱面數也可以理解為一個盤面上的磁道數
磁盤訪問時間 = 寫入控制器時間 + 尋道時間 + 旋轉延遲時間 + 傳輸時間
硬盤讀取數據時,讀寫磁頭沿徑向移動,移到要讀取的扇區所在磁道的上方,這段時間稱為尋道時間(seek time)。因讀寫磁頭的起始位置與目標位置之間的距離不同,尋道時間也不同。磁頭到達指定磁道后,然后通過盤片的旋轉,使得要讀取的扇區轉到讀寫磁頭的下方,這段時間稱為旋轉延遲時間(rotational latencytime)。然后再讀寫數據,讀寫數據也需要時間,這段時間稱為傳輸時間(transfer time)
直接通過三維參數(柱面、磁頭、扇區)使用磁盤是非常麻煩的。從扇區號到盤塊號,是對磁盤使用的第一層抽象,本章節主要介紹如何通過盤塊號來使用磁盤,其中2.1節介紹盤塊號如何轉換為三維參數,2.2節分析Linux0.11中生磁盤的使用過程。
扇區大小固定,但操作系統可以每次讀/寫連續的幾個扇區,這幾個連續的扇區可以組成一個盤塊。為方便理解如何通過盤塊號來訪問磁盤,假定一個盤塊的大小為一個扇區,盤塊號排列方式與扇區號一樣(即盤塊號與圖1.4的扇區號排列一致)。根據這種排列方式,就可以根據盤塊號計算出要訪問的三維參數(C,H,S):
// 根據要訪問的三維參數可以計算出對應的盤塊號
block = C * (Heads * Sectors) + H * Sectors + S;
// 也可以通過盤塊號計算出要訪問的三維參數
S = block % Sectors; // 注意向磁盤控制器內寫入的扇區號是真實磁盤的物理扇區號,不是上圖中的扇區號
C = block / ( Heads * Sectors);
H = (block % ( Heads * Sectors)) / Sectors;
其中 C,H,S 表示訪問磁盤所需要的三維參數;block 表示盤塊號;Heads 為常量,表示磁盤的磁頭數(也就是盤面數);Sectors 為常量,表示一個磁道上的扇區數;若一個盤塊對應n個扇區,計算方式也與之類似。
盤塊大小的分配也會影響到磁盤使用的效率:盤塊越大,讀寫的速度會越快,但碎片也會越大(這些碎片無法使用);盤塊越小讀寫速度會越慢,但碎片會越小。
假設磁頭的初始位置是100號磁道,有多個進程先后陸續的請求訪問55,58,39,18,90,160,150,38,184號磁道
根據進程請求訪問磁盤的先后順序進行調度
按照FCFS的規則,按照請求到達的順序,磁頭需要一次移動到55,58,39,90,160,150,38,184號磁道
磁頭總共移動的磁道個數為45+3+19+21+72+70+10+112+146=498
平均尋道長度為498/9=55.3個磁道
最短尋道時間優先,其要求訪問的磁道與當前磁頭所在的磁道距離最近,以便每次的尋道時間最短,但這種調度算法卻不能保證平均尋道時間最短
假設磁頭的初始位置是100號磁道,有多個進程先后陸續的請求訪問55,58,39,18,90,160,150,38,184號磁道
按照SSTF的規則,請求到達的
磁頭總共移動了(100-18)+(184-18)=248個磁道
平均尋道長度為248/9=27.5個磁道
當磁頭正在由里向外移動時,SCAN算法所選擇的下一個訪問對象應是其欲訪問的磁道,既在當前磁道之外,又是距離最近的。這樣由里向外地訪問,直至再無更外的磁道需要訪問時,才將磁臂換向,由外向里移動。也叫電梯算法。
磁頭總共移動了(184-100)+(184-18)=250個磁道
平均尋道長度為250/9=27.8個磁道
為了減少SCAN算法造成的某些進程的請求被嚴重推遲,CSCAN算法規定磁頭單向移動。
磁頭總共移動了(184-100)+(184-18)+(90-18)=322個磁道
平均尋道長度為322/9=35.8個磁道
本節主要介紹 Linux0.11 中生磁盤的使用過程。本節內容只是對生磁盤的使用過程進行了粗糙的分析,主要是為了了解生磁盤的大致使用過程,如對 請求項隊列的數據結構:struct buffer_head struct request struct blk_dev_struct三個結構體是怎么配合工作的等等, 這些細節部分都沒有進行說明,對于這些細節建議參考《Linux內核完全剖析——基于0.12內核》。
生磁盤的使用過程簡圖如下:
下面根據 Linux0.11 的程序對生磁盤的使用過程進行分析。
(1)進程向緩沖區管理程序提出讀寫磁盤申請。緩沖區管理程序做出一個磁盤請求,在將請求加入請求隊列中,最后讓進程進入睡眠等待狀態
/*創建請求項,并加入到請求隊列中*/
/*major為主設備號,rw為命令(讀/寫),bh為存放數據的緩沖區頭指針*/
static void make_request(int major,int rw, struct buffer_head * bh)
{
struct request * req;
int rw_ahead;
......
/* fill up the request-info, and add it to the queue */
/*前面主要是在尋找空閑請求項,并將請求項插入到請求項隊列的指定位置*/
/*接下來就是做出請求項,從下面這段程序可以看出 bh 中所包含的信息*/
req->dev = bh->b_dev; /*數據源的主設備號*/
req->cmd = rw;
req->errors=0;
/*bh->b_blocknr 應該就是圖1.4中說的扇區號,而req->sector就是盤塊號。
從這里可以看出一個盤塊的大小為2個扇區(2*512Bety)*/
req->sector = bh->b_blocknr<<1;
req->nr_sectors = 2; /*本次請求的扇區數*/
req->buffer = bh->b_data;/*將請求項的緩沖區指針指向需要讀寫的數據緩沖區*/
req->waiting = NULL;
req->bh = bh;
req->next = NULL;
add_request(major+blk_dev,req);/*將請求項加入隊列*/
}
在將請求項插入請求隊列時,為了讓磁盤使用的更加高效,這里采用了電梯算法將請求項插入請求隊列。
/*本函數是將已經做好的請求項(req),加入到請求隊列(dev)中*/
static void add_request(struct blk_dev_struct * dev, struct request * req)
{
struct request * tmp;
req->next = NULL;
cli();/*這段代碼要互斥,因此關中斷*/
if (req->bh)
req->bh->b_dirt = 0;
if (!(tmp = dev->current_request)) {/*將tmp指向請求隊列的隊首*/
/*若當前設備的請求項列表為空則設置 req 為當前請求項,并立即調用設備請求項處理函數*/
dev->current_request = req;
sti();
(dev->request_fn)();/*設備請求項處理函數指針,若當前請求讀寫的為硬盤,則它是 do_hd_request() */
return;
}
/*如果當前設備的請求項列表不為空則將 req 插入請求隊列中*/
/*下面這個for循環將利用 電梯算法 將req插到dev的合適位置*/
for ( ; tmp->next ; tmp=tmp->next)/*從前往后掃描整個請求隊列*/
if ((IN_ORDER(tmp,req) || /*IN_ORDER應該是要比較tem的扇區號是否小于req的扇區號的,不過這里簡化了,比較的是柱面號*/
!IN_ORDER(tmp,tmp->next)) &&
IN_ORDER(req,tmp->next))
break;
req->next=tmp->next;
tmp->next=req;
sti();/*開中斷*/
}
將請求項插入請求隊列后就會讓進程進入睡眠等待狀態,不過我還沒找到相關代碼,找到后在補充這一段。
(2) 根據(1)中算出的盤塊號計算出要訪問的三維參數(扇區號,柱面,磁頭)并寫入磁盤控制器
從(1)中可以看出,對于硬盤的請求項,設備請求項處理函數為 do_hd_request() 。
/*本函數執行磁盤讀寫請求操作*/
/*該函數首先根據請求項中的設備號和盤塊號等信息計算出要訪問的磁盤三維參數。
然后根據請求項中的讀寫命令,向磁盤控制器發出相應的讀寫命令。*/
void do_hd_request(void)
{
int i,r = 0;
unsigned int block,dev;
unsigned int sec,head,cyl;
unsigned int nsect;
INIT_REQUEST;
dev = MINOR(CURRENT->dev);
block = CURRENT->sector;/*CURRENT->sector為盤塊號*/
if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) {
end_request(0);
goto repeat;
}
block += hd[dev].start_sect;/*start_sect 是分區在磁盤中的起始物理(絕對)扇區,這個和磁盤分區有關,先不管它*/
dev /= 5;
/*下面這段內嵌匯編將根據盤塊號算出cyl, head, sec(CHS)*/
__asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
"r" (hd_info[dev].sect));
__asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
"r" (hd_info[dev].head));
sec++;
nsect = CURRENT->nr_sectors;/*nr_sectors 是分區中的扇區總數*/
......
if (CURRENT->cmd == WRITE) {
/*發送寫磁盤命令。write_intr 中斷調用函數,當前中斷為寫操作時被設置成中斷過程中調用的 C 函數。
磁盤完成寫盤命令后會向CPU發送中斷請求信號,于是在磁盤控制器完成寫操作后會立刻調用該函數*/
hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
......
} else if (CURRENT->cmd == READ) {
/*發送讀磁盤命令。read_intr 的用法與 write_intr 類似*/
hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);
} else
panic("unknown hd-command");
}
static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
unsigned int head,unsigned int cyl,unsigned int cmd,
void (*intr_addr)(void))
{
register int port asm("dx");
......
do_hd = intr_addr; /*do_hd = intr_addr 在磁盤中斷處理函數(hd_interrupt:)中被調用*/
outb_p(hd_info[drive].ctl,HD_CMD); /*向磁盤控制器中輸出控制字節*/
port=HD_DATA;
outb_p(hd_info[drive].wpcom>>2,++port);
outb_p(nsect,++port); //參數,讀寫扇區總數
outb_p(sect,++port); //參數,起始扇區
outb_p(cyl,++port); //參數,柱面號低8位
outb_p(cyl>>8,++port); //參數,柱面號高8位
outb_p(0xA0|(drive<<4)|head,++port); //參數,驅動器號加磁頭號
outb(cmd,++port); //命令,磁盤控制命令
}
outb_p() 會執行一段匯編代碼, 里面很重要的一句 :outb %%al,%%dx,就是向磁盤端口寫數據。
(3)磁盤中斷請求處理
當磁盤處理完成或發生錯誤是就會發出中斷信號,此時CPU響應中斷請求,并調用磁盤中斷處理程序:hd_interrupt:
hd_interrupt:
......
1: jmp 1f
1: xorl %edx,%edx
xchgl do_hd,%edx #do_hd是一個函數指針,被賦值為 write_intr() 或 read_intr(),
#看一下前面提到的 hd_out() 調用過程就會明白了。這里將edx設置為 do_hd
testl %edx,%edx
jne 1f
movl $unexpected_hd_interrupt,%edx
1: outb %al,$0x20
call *%edx # "interesting" way of handling intr.
# 調用 “edx” 函數
......
iret
(4)磁盤處理完成產生中斷,CPU處理中斷并將磁盤返回數據加入緩沖區,最后喚醒進程
read_intr() 函數會將磁盤控制器中的數據復制到請求項指定的緩沖區中。在執行read_intr() 時會調用函數 end_request(1), 該函數會將進程喚醒。write_intr() 的處理過程與 read_intr() 類似。
到此如何通過盤塊使用磁盤就分析完畢了。不過用盤塊號來使用磁盤是相當麻煩的,程序員忍忍也就算了,用戶怎能受得了如此折磨。下一章將介紹如何通過文件來使用磁盤。
圖解