濮阳杆衣贸易有限公司

主頁(yè) > 知識(shí)庫(kù) > Linux折騰記(八):使用GCC和GNU Binutils編寫能在x86實(shí)模式運(yùn)行的16位代碼

Linux折騰記(八):使用GCC和GNU Binutils編寫能在x86實(shí)模式運(yùn)行的16位代碼

熱門標(biāo)簽:壽光百度地圖標(biāo)注中心網(wǎng)站 新鄭電銷外呼系統(tǒng)線路 怎樣給景區(qū)加百度地圖標(biāo)注 電話機(jī)器人哪里有賣的 河南智能電話機(jī)器人公司 地球地圖標(biāo)注方法 樺甸電銷機(jī)器人 河北語(yǔ)音電銷機(jī)器人 商戶地圖標(biāo)注

  不可否認(rèn),這次的標(biāo)題有點(diǎn)長(zhǎng)。之所以把標(biāo)題寫得這么詳細(xì),主要是為了搜索引擎能夠準(zhǔn)確地把確實(shí)需要了解GCC生成16位實(shí)模式代碼方法的朋友帶到我的博客。先說(shuō)一下背景,編寫能在x86實(shí)模式下運(yùn)行的16位代碼,這個(gè)話題確實(shí)有點(diǎn)復(fù)古,所以能找到的資料也相應(yīng)較少。要運(yùn)行x86實(shí)模式的程序,目前我知道的只有兩種方式,一種是使用DOS系統(tǒng),另一種是把它寫成引導(dǎo)扇區(qū)的代碼,在系統(tǒng)啟動(dòng)時(shí)直接運(yùn)行。很顯然,許多講自己實(shí)現(xiàn)操作系統(tǒng)的書籍都會(huì)講到x86實(shí)模式,也只有自己實(shí)現(xiàn)操作系統(tǒng)引導(dǎo)的朋友需要用到x86實(shí)模式,所以我這篇文章的閱讀用戶數(shù)肯定很少,雖然我自認(rèn)為它填補(bǔ)了網(wǎng)上關(guān)于該話題相關(guān)資料缺乏的空白。因此,凡是逛到我這篇文章的朋友,請(qǐng)點(diǎn)一下推薦,謝謝。

  為什么說(shuō)我這篇博客填補(bǔ)了相關(guān)話題的空白呢?那是因?yàn)椴还苁悄切憰模€是網(wǎng)上寫文章的,一旦需要編寫16位的實(shí)模式代碼,都喜歡拿NASM說(shuō)事兒,一點(diǎn)也不顧GNU AS的感受。當(dāng)然,這是有歷史原因的,因?yàn)長(zhǎng)inux自從其誕生起就是32位,就是多用戶多任務(wù)操作系統(tǒng),所以GCC和Gnu AS一移植到Linux上就是用來(lái)編寫32位保護(hù)模式的代碼的。而且,ELF可執(zhí)行文件格式也只有ELF32和ELF64,沒(méi)聽(tīng)說(shuō)過(guò)有ELF16的。即使是Linux自己,剛誕生的時(shí)候(1991年),也只有使用as86匯編器來(lái)編寫自己的16位啟動(dòng)代碼,直到1995年以后,GNU AS才逐步加入編寫16位代碼的能力。

  下面開(kāi)始我的GCC和GNU Binutils的16位代碼之旅。我決定使用DOS作為我的測(cè)試環(huán)境,所以最后生成的可執(zhí)行文件都把它制作成DOS系統(tǒng)中可運(yùn)行的Plain Binary格式。第一步安裝一個(gè)qemu虛擬機(jī)來(lái)運(yùn)行FreeDOS,安裝虛擬機(jī)在Ubuntu中只需要一個(gè)sudo apt-get install qemu命令就可以完成,所以我就不截圖了。但是FreeDOS的軟盤映像文件需要到Qemu的官網(wǎng)上面去下載,下載地址如下圖:

  使用qemu-system-i386 -fda freedos.img可以運(yùn)行Qemu虛擬機(jī)和FreeDOS系統(tǒng),如下圖:

  因?yàn)閰R編語(yǔ)言更接近底層,而C語(yǔ)言更高級(jí),所以先從匯編語(yǔ)言開(kāi)始,逐步過(guò)渡到C語(yǔ)言。先寫一個(gè)簡(jiǎn)單的、能在DOS中顯示一個(gè)“Hello,world!”的匯編語(yǔ)言程序,考慮到我之后會(huì)使用該程序調(diào)用C語(yǔ)言的main函數(shù),并且該程序負(fù)責(zé)讓程序運(yùn)行結(jié)束后順利返回DOS系統(tǒng),所以我把這個(gè)程序命名為test_code16_startup.s。其代碼如下:

  下面對(duì)以上代碼進(jìn)行簡(jiǎn)單解釋:

  1. GNU AS匯編器使用的匯編語(yǔ)言采用的是ATT語(yǔ)法,該語(yǔ)法和Intel語(yǔ)法不同。我更喜歡ATT的語(yǔ)法,原因有兩個(gè),一是ATT語(yǔ)法是Linux世界中通用的標(biāo)準(zhǔn),二是ATT語(yǔ)法在某些概念方面確實(shí)理解起來(lái)更簡(jiǎn)單(比如內(nèi)存尋址模式)。有匯編語(yǔ)言基礎(chǔ)的人,ATT語(yǔ)法學(xué)起來(lái)也很快,主要有以下幾條:①匯編指令后面跟有操作數(shù)長(zhǎng)度的后綴,比如mov指令,如果操作數(shù)是8位,則用movb,如果操作數(shù)是16位,則用movw,如果操作數(shù)是32位,則用movl,如果操作數(shù)是64位,則用movq,其余指令依此類推;②操作數(shù)的順序是源操作數(shù)在前,目標(biāo)操作數(shù)在后,比如movw %cs, %ax表示把cs寄存器中的數(shù)據(jù)移動(dòng)到ax寄存器中,這個(gè)順序和Intel匯編語(yǔ)法正好相反;③所有的寄存器使用%前綴,如%ax, %di, %esp等;④對(duì)于立即數(shù),需要使用$前綴,比如 $4,  $0x0c,而且如果一個(gè)數(shù)字是以0開(kāi)頭,則是8進(jìn)制,以其它數(shù)字開(kāi)頭,是10進(jìn)制,以0x開(kāi)頭則是16進(jìn)制,標(biāo)號(hào)當(dāng)立即數(shù)使用時(shí),需要$前綴,比如上面的pushw $message,而標(biāo)號(hào)當(dāng)函數(shù)名使用時(shí),不需要$前綴,比如上面的callw display_str;⑤內(nèi)存尋址方式,眾所周知,x86尋址方式眾多,什么直接尋址、間接尋址、基址尋址、基址變址尋址等等讓人眼花繚亂,而ATT語(yǔ)法對(duì)內(nèi)存尋址方式做了一個(gè)很好的統(tǒng)一,其格式為section:displacement(base, index, scale),其中section是段地址,displacement是位移,base是基址寄存器,index是索引,scale是縮放因子,其計(jì)算方式為線性地址=section + displacement + base + index*scale,最重要的是,可以省略以上格式中的一個(gè)或多個(gè)部分,比如movw 4, %ax就是把內(nèi)存地址4中的值移動(dòng)到ax寄存器中,movw 4(%esp), %ax就是把esp+4指向的地址中的值移動(dòng)到ax寄存器中,依此類推。我上面的介紹是不是全網(wǎng)絡(luò)最簡(jiǎn)明的ATT匯編語(yǔ)法教程?

  2. 在以上代碼中我全部使用的都是16位的指令,如movw、pushw、callw等,并且直接在代碼中定義了字符串“Hello, world!”。

  3. 在以上代碼中使用了函數(shù)display_str,在調(diào)用display_str之前,我使用pushw $15和pushw $message將參數(shù)從右向左依次壓棧,然后使用callw指令調(diào)用函數(shù),這和C語(yǔ)言的函數(shù)調(diào)用約定是一樣的。調(diào)用callw指令會(huì)自動(dòng)將%ip寄存器壓棧,而在函數(shù)開(kāi)始時(shí),我又用pushw %bp將%bp寄存器壓棧,所以%esp又向下移動(dòng)了4個(gè)字節(jié),所以在函數(shù)中使用0x4(%esp)和0x6(%esp)可以訪問(wèn)到這兩個(gè)參數(shù)。在32位代碼中,由于調(diào)用函數(shù)時(shí)壓棧的是%eip和%ebp,所以需要使用0x8(%esp)和0xc(%esp)來(lái)依次訪問(wèn)壓棧的參數(shù)。關(guān)于匯編語(yǔ)言函數(shù)調(diào)用的細(xì)節(jié),我這里有一本好書Linux匯編編程指南.pdf。這是一本免費(fèi)的英文版電子書,其原名為《Programming from the ground up》。

  4. 以上代碼使用BIOS中斷int 0x10來(lái)輸出字符串,使用DOS中斷int 0x21來(lái)返回DOS系統(tǒng)。

  5. 最重要的是,需要使用.code16指令讓匯編器將程序匯編成16位的代碼。

  代碼完成后,使用下面一串命令就可以把它進(jìn)行匯編、鏈接,然后轉(zhuǎn)換成DOS下的純二進(jìn)制格式(Plain Binary),最后復(fù)制到FreeDOS.img中,使用Qemu虛擬機(jī)執(zhí)行FreeDOS,然后運(yùn)行該16位實(shí)模式程序。這一串命令及其運(yùn)行效果如下圖:

  這些命令中比較重要的選項(xiàng)我都特意標(biāo)出來(lái)了。由于我用的是64位的環(huán)境,所以調(diào)用as命令的時(shí)候需要指定--32選項(xiàng),調(diào)用ld命令的時(shí)候需要指定-m elf_i386選項(xiàng)。指定以上選項(xiàng)后,生成的是32位的ELF目標(biāo)文件,否則默認(rèn)會(huì)生成64位的ELF目標(biāo)文件,如果目標(biāo)文件是64位,以后和C語(yǔ)言生成的目標(biāo)文件連接時(shí)會(huì)出問(wèn)題。使用32位環(huán)境的朋友們不用特意指定這兩個(gè)選項(xiàng)。由于DOS系統(tǒng)總是把Plain Binary文件載入到0x100地址處執(zhí)行,所以調(diào)用ld命令時(shí),需要指定-Ttext 0x100選項(xiàng)。ld命令執(zhí)行完成后,生成的是ELF格式的可執(zhí)行文件test.elf,最后需要調(diào)用objcopy生成純二進(jìn)制文件,-j .text選項(xiàng)的意思是只需要代碼段,因?yàn)槲野?ldquo;Hello, world!”也是定義在代碼段中的,-O binary選項(xiàng)指定輸出格式為純二進(jìn)制文件,輸出文件為test.com。最后,將freedos.img鏡像文件mount到Ubuntu中,將test.com拷貝到其中,然后umount,然后運(yùn)行虛擬機(jī),在DOS中運(yùn)行test,就可以看到效果了。

  除了as和ld,GNU Binutils中的其它程序也是寫程序和分析程序時(shí)的好幫手??梢允褂胷eadelf -S查看test.elf文件中的所有段,也可以使用objdump -s命令將test.elf中的數(shù)據(jù)以16進(jìn)制形式輸入,如下圖:

  當(dāng)然,也可以使用objdump -d或者objdump -D將程序進(jìn)行反匯編,查看是否真正生成了16位代碼,如下圖:(反匯編時(shí)一定要指定-m i8086選項(xiàng))

  也可以對(duì)純二進(jìn)制格式的文件進(jìn)行反匯編,必須指定-b binary選項(xiàng),如下圖,對(duì)test.com進(jìn)行反匯編:

  反匯編時(shí),一定要指定-m i8086選項(xiàng),否則objdump不知道反匯編的是16位代碼。(前面提到過(guò)Linux從誕生起就是32位,所以ELF只有32位和64位兩種,沒(méi)有16位的ELF格式。)如下圖,如果使用-m i386選項(xiàng)進(jìn)行反匯編,反匯編結(jié)果將不知所云:

  下面進(jìn)入C語(yǔ)言的世界。為了搞清楚C語(yǔ)言生成的16位代碼的匯編指令有哪些特別之處,先寫一個(gè)簡(jiǎn)單的C語(yǔ)言程序進(jìn)行調(diào)研,如下圖:

  該程序有以下特點(diǎn):

  1. 程序的開(kāi)頭使用了__asm__(".code16\n")嵌入?yún)R編指令,以指示as生成16位代碼;

  2. display_str函數(shù)的簽名和之前匯編語(yǔ)言中的相同,可以使用它來(lái)觀察C語(yǔ)言生成的代碼如何傳遞參數(shù)。

  使用下面的命令對(duì)程序進(jìn)行編譯和反匯編,如下圖:

  從上圖可以看出,C語(yǔ)言生成的代碼雖然是16位,但是它有如下特點(diǎn):①?gòu)纳傻膁isplay_str函數(shù)中可以看出,函數(shù)一開(kāi)始是push %ebp,而不是push %bp;②在display_str函數(shù)中獲取參數(shù)的位置分別為0x8(%ebp)和0xc(%ebp),而不是我在匯編語(yǔ)言中寫的0x4(%ebp)和0x6(%ebp);③從生成的main函數(shù)可以看出,調(diào)用diaplay_str之前,沒(méi)有使用push命令把參數(shù)壓棧,而是直接通過(guò)sub $0x18, %esp調(diào)整%esp的位置,然后使用mov指令將參數(shù)放到指定位置,和使用push指令的效果相同;④雖然我在display_str函數(shù)的定義中故意將長(zhǎng)度參數(shù)定義為short,但是從生成的代碼中可以看到依然是每隔4個(gè)字節(jié)放一個(gè)參數(shù)。

  另外需要說(shuō)明的是,調(diào)用gcc時(shí)除了指定-c選項(xiàng)指示它只編譯不連接外,還要指定-m32選項(xiàng),這樣才會(huì)生成32位的匯編代碼,而只有在32位的匯編代碼中使用.code16指令,才能編譯成16位的機(jī)器碼。如果沒(méi)有指定-m32選項(xiàng),則生成的是64位匯編代碼,然后匯編時(shí)會(huì)出錯(cuò)。使用-m32選項(xiàng)后,生成的目標(biāo)文件是ELF32格式。ELF32格式的目標(biāo)文件只能和ELF32格式的目標(biāo)文件連接,這也是為什么前面的as和ld需要指定--32和-m elf_i386選項(xiàng)的原因。

  通過(guò)以上分析,似乎可以得出以下結(jié)論:只需要將匯編代碼中的pushw %bp更改為pushl %ebp,然后將獲取參數(shù)的位置調(diào)整為0x8(%ebp)和0xc(%ebp),就可以從C語(yǔ)言里面成功調(diào)用到匯編語(yǔ)言中的函數(shù)了。而事實(shí)上,還有一點(diǎn)點(diǎn)小差距。從上面的反匯編代碼中可以看到,函數(shù)調(diào)用時(shí)使用的是16位的call指令,該指令壓棧的是%ip,而不是%eip,而C語(yǔ)言生成的函數(shù)框架中獲取的參數(shù)位置是按照將%eip壓棧計(jì)算出來(lái)的,它們之間差了兩個(gè)字節(jié)。

  為了證明我以上判斷的準(zhǔn)確性,我將上面的C語(yǔ)言程序和匯編程序修改后,編譯連接成一個(gè)完整的程序,看看它究竟能否正確運(yùn)行。如下圖:

  C語(yǔ)言程序修改很簡(jiǎn)單,就是去掉了display_str函數(shù)的實(shí)現(xiàn),只保留聲明。匯編代碼如下圖:

  匯編語(yǔ)言的更改包含以下幾個(gè)地方:將display_str函數(shù)導(dǎo)出,將pushw %bp改為pushl %ebp,同時(shí)修改獲取參數(shù)的位置。編譯、連接、運(yùn)行程序的指令如下:

  可以看到“Hello world from C language”沒(méi)有正確顯示出來(lái)。上面的命令都是前面用過(guò)的,不需要多解釋,唯一不同的是使用C語(yǔ)言寫的程序多了一個(gè).rodata段,所以在objcopy的時(shí)候需要把這個(gè)段也包含進(jìn)來(lái)。

  由于C語(yǔ)言生成的函數(shù)框架都是從0x8(%ebp)開(kāi)始取參數(shù),它認(rèn)為0x0(%ebp)是old ebp,0x4(%ebp)是%eip,而事實(shí)上使用16位的call指令調(diào)用函數(shù)后,0x4(%ebp)中是%ip而不是%eip,所以要從0x6(%ebp)開(kāi)始取參數(shù)。我們不可能修改C語(yǔ)言生成的函數(shù)框架,只能看看能否將16位的call改成32位的call。

  辦法當(dāng)然是有的,那就是不使用.code16,而使用.code16gcc。.code16gcc和.code16不同的地方就在于它生成的匯編代碼在使用到call、ret、jump等指令時(shí),都生成32位的機(jī)器碼,相當(dāng)于calll,retl,jumpl。這也是.code16gcc叫.code16gcc的原因,因?yàn)樗褪桥浜螱CC生成的函數(shù)框架使用的。

  下面再來(lái)修改代碼,C語(yǔ)言代碼修改很簡(jiǎn)單,只需要將.code16改成.code16gcc即可,如下圖:

  通過(guò)反匯編,可以看到它使用了32位的calll和retl,如下圖:

  匯編程序的修改主要是將.code16改為.code16gcc,然后手動(dòng)將callw改成calll,將retw改成retl,如下圖:

  最后,編譯連接,拷貝到freedos.img,運(yùn)行虛擬機(jī),查看運(yùn)行效果,如下圖:

  大功告成,運(yùn)行效果如上圖。

總結(jié):

  編寫運(yùn)行于x86實(shí)模式下的16位代碼是一個(gè)很復(fù)古的話題,編寫能在DOS下運(yùn)行的Plain Binary可執(zhí)行文件是一個(gè)更復(fù)古的話題。以往,凡是需要使用x86的16位實(shí)模式的時(shí)候,作者都喜歡用NASM來(lái)編程。比如《30天自制操作系統(tǒng)》、《Orange's 一個(gè)操作系統(tǒng)的實(shí)現(xiàn)》、《x86匯編語(yǔ)言——從實(shí)模式到保護(hù)模式》等書籍都以NASM匯編器和Intel匯編語(yǔ)法作為示例。而且他們都是在進(jìn)入32位保護(hù)模式后,才讓匯編語(yǔ)言和C語(yǔ)言共同工作。

  我用Linux操作系統(tǒng),所以我就是想不管是寫32位代碼,還是16位代碼,都能使用GCC和GNU AS。我還想即使是在16位模式下,也能盡量少用匯編語(yǔ)言,多用C語(yǔ)言。經(jīng)過(guò)努力,有了上面的文章。使用GCC和GNU Binutils編寫運(yùn)行于x86實(shí)模式的16位代碼的過(guò)程如下:

  1. 如果只用匯編語(yǔ)言編寫16位程序,請(qǐng)使用.code16指令,并保證只使用16位的指令和寄存器;如果要和C語(yǔ)言一起工作,請(qǐng)使用.code16gcc指令,并且在函數(shù)框架中使用pushl,calll,retl,leavel,jmpl,使用0x8(%ebp)開(kāi)始訪問(wèn)函數(shù)的參數(shù);很顯然,使用C語(yǔ)言和匯編語(yǔ)言混編的程序可以在實(shí)模式下運(yùn)行,但是不能在286之前的真實(shí)CPU上運(yùn)行,因?yàn)?86之前的CPU還沒(méi)有pushl、calll、retl、leavel、jmpl等指令。

  2. 使用as時(shí),請(qǐng)指定--32選項(xiàng),使用gcc時(shí),請(qǐng)指定-m32選項(xiàng),使用ld時(shí),請(qǐng)指定-m elf_i386選項(xiàng)。如果是反匯編16位代碼,在使用objdump時(shí),請(qǐng)使用-m i8086選項(xiàng)。

  3. 在DOS中運(yùn)行的.com文件會(huì)被加載到0x100處執(zhí)行,所以使用ld連接時(shí)需指定-Ttext 0x100選項(xiàng);引導(dǎo)扇區(qū)的代碼會(huì)被加載到0x7c00處執(zhí)行,所以使用ld連接時(shí)需指定-Ttext 0x7c00選項(xiàng)。

  4. 使用gcc、as、ld生成的程序默認(rèn)都是ELF格式,而在DOS下運(yùn)行的.com程序是Plain Binary的,在引導(dǎo)扇區(qū)運(yùn)行的代碼也是Plain Binary的,所以需要使用objcopy將ELF文件中的代碼段和數(shù)據(jù)段拷貝到一個(gè)Plain Binary文件中,使用-O binary選項(xiàng); Plain Binary文件也可以反匯編,在使用objdump時(shí)需指定-b binary選項(xiàng)。

標(biāo)簽:來(lái)賓 淄博 忻州 楚雄 遼陽(yáng) 阜陽(yáng) 荊州 迪慶

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《Linux折騰記(八):使用GCC和GNU Binutils編寫能在x86實(shí)模式運(yùn)行的16位代碼》,本文關(guān)鍵詞  Linux,折騰,記,八,使用,GCC,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《Linux折騰記(八):使用GCC和GNU Binutils編寫能在x86實(shí)模式運(yùn)行的16位代碼》相關(guān)的同類信息!
  • 本頁(yè)收集關(guān)于Linux折騰記(八):使用GCC和GNU Binutils編寫能在x86實(shí)模式運(yùn)行的16位代碼的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章
    南康市| 凯里市| 财经| 凤庆县| 苏州市| 阳春市| 东莞市| 奈曼旗| 平潭县| 石首市| 平江县| 土默特左旗| 三门县| 唐河县| 全南县| 观塘区| 吉木萨尔县| 塘沽区| 宜春市| 双桥区| 大宁县| 犍为县| 孟连| 南召县| 宣威市| 民乐县| 天津市| 赤壁市| 铁力市| 寿光市| 西吉县| 嘉峪关市| 马关县| 大竹县| 册亨县| 株洲市| 永修县| 丰台区| 江达县| 高密市| 临颍县|