不可否認,這次的標題有點長。之所以把標題寫得這麼詳細,主要是為了搜索引擎能夠准確地把確實需要了解GCC生成16位實模式代碼方法的朋友帶到我的博客。先說一下背景,編寫能在x86實模式下運行的16位代碼,這個話題確實有點復古,所以能找到的資料也相應較少。要運行x86實模式的程序,目 前我知道的只有兩種方式,一種是使用DOS系統,另一種是把它寫成引導扇區的代碼,在系統啟動時直接運行。很顯然,許多講自己實現操作系統的書籍都會講到 x86實模式,也只有自己實現操作系統引導的朋友需要用到x86實模式,所以我這篇文章的閱讀用戶數肯定很少,雖然我自認為它填補了網上關於該話題相關資 料缺乏的空白。因此,凡是逛到我這篇文章的朋友,請點一下推薦,謝謝。
為什麼說我這篇博客填補了相關話題的空白呢?那是因為不管是那些寫書的,還是網上寫文章的,一旦需要編寫16位的實模式代碼,都喜歡拿NASM 說事兒,一點也不顧GNU AS的感受。當然,這是有歷史原因的,因為Linux自從其誕生起就是32位,就是多用戶多任務操作系統,所以GCC和Gnu AS一移植到Linux上就是用來編寫32位保護模式的代碼的。而且,ELF可執行文件格式也只有ELF32和ELF64,沒聽說過有ELF16的。即使 是Linux自己,剛誕生的時候(1991年),也只有使用as86匯編器來編寫自己的16位啟動代碼,直到1995年以後,GNU AS才逐步加入編寫16位代碼的能力。
Ubuntu 12.04嵌入式交叉編譯環境arm-linux-GCC搭建過程圖解 http://www.linuxidc.com/Linux/2013-06/85902.htm
Ubuntu 12.10安裝交叉編譯器arm-none-linux-gnueabi-GCC http://www.linuxidc.com/Linux/2013-03/82016.htm
Ubuntu下Vim+GCC+GDB安裝及使用 http://www.linuxidc.com/Linux/2013-01/78159.htm
Ubuntu下兩個GCC版本切換 http://www.linuxidc.com/Linux/2012-10/72284.htm
下面開始我的GCC和GNU Binutils的16位代碼之旅。我決定使用DOS作為我的測試環境,所以最後生成的可執行文件都把它制作成DOS系統中可運行的Plain Binary格式。第一步安裝一個qemu虛擬機來運行FreeDOS,安裝虛擬機在Ubuntu中只需要一個sudo apt-get install qemu命令就可以完成,所以我就不截圖了。但是FreeDOS的軟盤映像文件需要到Qemu的官網上面去下載,下載地址如下圖:
使用qemu-system-i386 -fda freedos.img可以運行Qemu虛擬機和FreeDOS系統,如下圖:
因為匯編語言更接近底層,而C語言更高級,所以先從匯編語言開始,逐步過渡到C語言。先寫一個簡單的、能在DOS中顯示一個 “Hello,world!”的匯編語言程序,考慮到我之後會使用該程序調用C語言的main函數,並且該程序負責讓程序運行結束後順利返回DOS系統, 所以我把這個程序命名為test_code16_startup.s。其代碼如下:
下面對以上代碼進行簡單解釋:
1. GNU AS匯編器使用的匯編語言采用的是AT&T語法,該語法和Intel語法不同。我更喜歡AT&T的語法,原因有兩個,一是 AT&T語法是Linux世界中通用的標准,二是AT&T語法在某些概念方面確實理解起來更簡單(比如內存尋址模式)。有匯編語言基礎的 人,AT&T語法學起來也很快,主要有以下幾條:①匯編指令後面跟有操作數長度的後綴,比如mov指令,如果操作數是8位,則用movb,如果操 作數是16位,則用movw,如果操作數是32位,則用movl,如果操作數是64位,則用movq,其余指令依此類推;②操作數的順序是源操作數在前, 目標操作數在後,比如movw %cs, %ax表示把cs寄存器中的數據移動到ax寄存器中,這個順序和Intel匯編語法正好相反;③所有的寄存器使用%前綴,如%ax, %di, %esp等;④對於立即數,需要使用$前綴,比如 $4, $0x0c,而且如果一個數字是以0開頭,則是8進制,以其它數字開頭,是10進制,以0x開頭則是16進制,標號當立即數使用時,需要$前綴,比如上面的pushw $message,而標號當函數名使用時,不需要$前綴,比如上面的callw display_str;⑤內存尋址方式,眾所周知,x86尋址方式眾多,什麼直接尋址、間接尋址、基址尋址、基址變址尋址等等讓人眼花缭亂,而AT&T語法對內存尋址方式做了一個很好的統一,其格式為section:displacement(base, index, scale), 其中section是段地址,displacement是位移,base是基址寄存器,index是索引,scale是縮放因子,其計算方式為線性地 址=section + displacement + base + index*scale,最重要的是,可以省略以上格式中的一個或多個部分,比如movw 4, %ax就是把內存地址4中的值移動到ax寄存器中,movw 4(%esp), %ax就是把esp+4指向的地址中的值移動到ax寄存器中,依此類推。我上面的介紹是不是全網絡最簡明的AT&T匯編語法教程?
2. 在以上代碼中我全部使用的都是16位的指令,如movw、pushw、callw等,並且直接在代碼中定義了字符串“Hello, world!”。
3. 在以上代碼中使用了函數display_str,在調用display_str之前,我使用pushw $15和pushw $message 將參數從右向左依次壓棧,然後使用callw指令調用函數,這和C語言的函數調用約定是一樣的。調用callw指令會自動將%ip寄存器壓棧,而在函數開 始時,我又用pushw %bp將%bp寄存器壓棧,所以%esp又向下移動了4個字節,所以在函數中使用0x4(%esp)和0x6(%esp)可以訪問到這兩個參數。在32位 代碼中,由於調用函數時壓棧的是%eip和%ebp,所以需要使用0x8(%esp)和0xc(%esp)來依次訪問壓棧的參數。關於匯編語言函數調用的 細節,我這裡有一本好書Linux匯編編程指南.pdf。這是一本免費的英文版電子書,其原名為《Programming from the ground up》。
Linux匯編編程指南 PDF (英文)下載:
------------------------------------------分割線------------------------------------------
免費下載地址在 http://linux.linuxidc.com/
用戶名與密碼都是www.linuxidc.com
具體下載目錄在 /2014年資料/9月/23日/使用GCC和GNU Binutils編寫能在x86實模式運行的16位代碼
下載方法見 http://www.linuxidc.com/Linux/2013-07/87684.htm
------------------------------------------分割線------------------------------------------
4. 以上代碼使用BIOS中斷int 0x10來輸出字符串,使用DOS中斷int 0x21來返回DOS系統。
5. 最重要的是,需要使用.code16指令讓匯編器將程序匯編成16位的代碼。