使用GCC和GNU Binutils编写能在x86实模式运行的16位代码

不可否认,这次的标题有点长。之所以把标题写得这么详细,主要是为了搜索引擎能够准确地把确实需要了解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位代码的能力。

下面开始我的GCC和GNU Binutils的16位代码之旅。我决定使用DOS作为我的测试环境,所以最后生成的可执行文件都把它制作成DOS系统中可运行的Plain Binary格式。第一步安装一个qemu虚拟机来运行FreeDOS,安装虚拟机在Ubuntu中只需要一个sudo apt-get install qemu命令就可以完成,所以我就不截图了。但是FreeDOS的软盘映像文件需要到Qemu的官网上面去下载,下载地址如下图:

使用GCC和GNU Binutils编写能在x86实模式运行的16位代码

使用qemu-system-i386 -fda freedos.img可以运行Qemu虚拟机和FreeDOS系统,如下图:

使用GCC和GNU Binutils编写能在x86实模式运行的16位代码

因为汇编语言更接近底层,而C语言更高级,所以先从汇编语言开始,逐步过渡到C语言。先写一个简单的、能在DOS中显示一个 “Hello,world!”的汇编语言程序,考虑到我之后会使用该程序调用C语言的main函数,并且该程序负责让程序运行结束后顺利返回DOS系统, 所以我把这个程序命名为test_code16_startup.s。其代码如下:

使用GCC和GNU Binutils编写能在x86实模式运行的16位代码

下面对以上代码进行简单解释:

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 (英文)下载

------------------------------------------分割线------------------------------------------

具体下载目录在 /2014年资料/9月/23日/使用GCC和GNU Binutils编写能在x86实模式运行的16位代码

------------------------------------------分割线------------------------------------------

4. 以上代码使用BIOS中断int 0x10来输出字符串,使用DOS中断int 0x21来返回DOS系统。

5. 最重要的是,需要使用.code16指令让汇编器将程序汇编成16位的代码。

 

相关推荐