在我们开机的时候,BIOS会将我们的启动设备的前512字节的代码复制到0x7c00处,这时便会执行我们的引导代码,本次的引导代码如下:
1 .code16 #告诉编译器,这段代码将会在16位实模式下执行,如果在这里遇到对eax ebx等32位寄存器操作时,加上前缀0x66,因为16位和32位保护模式的译码方式不一样 2 .text 3 .globl _start #声明ld连接器入口_start 4 _start: 5 movw %cs,%ax #设置段寄存器 6 movw %ax,%ds 7 movw %ax,%es 8 call DispStr 9 1: 10 jmp 1b 11 12 DispStr: 13 movw $Strsize,%bx 14 movw (%bx),%ax #字符串大小 15 movw %ax,%cx 16 movw $BootMessage,%ax 17 movw %ax,%bp #es:bp 显示字符串地址 18 movw $0x1301,%ax #ah=0x13在teletype模式下显示字符串 al=1 显示属性在bl 19 movw $0x000c,%bx #显示颜色,背景色 20 movw $0x0000,%dx #dh:行 dl:列 21 int $0x10 22 ret 23 24 BootMessage: 25 .asciz "Hello,OS,World!" #以0结尾的字符串 26 27 Strsize: 28 .word .-BootMessage #'.'表示当前这一行的地址 29 30 .org 510 31 .word 0xaa55 #引导扇区标志
上面这段代码利用了BIOS提供的显示中断,这些中断是在BIOS检测阶段为我们建立的,中断向量分布在0-256*4字节处。
执行结果如下图所示:
下面主要说一下编译链接过程,编译和链接用了下面两条语句(编译环境centos7 64位):
as --32 -o bootsect.o bootsect.s ld -m elf_i386 --oformat binary -Ttext=0x7c00 -o bootsect.bin bootsect.o
编译器用的是as,用来将汇编语言编译成带有符号可重定向的二进制文件,这种文件一般不能直接执行,下面详细的说明一下这个过程,拿我们上面的那个例子来说,编译器在编译movw $BootMessage,%ax(其中w表示传送一个字,$表示立即数,%ax表示ax寄存器)的时候,它如何知道$BootMessage这个立即数是多少呢?其实这个时候它是不知道的,那么它在编译的时候就会将这个位置记录下来,在链接器工作的时候将再将这个位置填上对应的地址,上面的ld在链接的时候有一个选项-Ttext=0x7c00,这就告诉了链接器这段程序里面的标号的偏移量要加一个0x7c00,比如说上面的程序,BootMessage相对于程序的第一句有一个偏移量,假设BootMessage前面的程序占用了0x30个字节,这个时候$BootMessage就会被$0x7c30所代替,所以如果这个地址指定错误(也可以在程序前面加ORG 0x7c00),那么程序将不会正常运行,因为程序被加载的地址就是0x7c00,这样我们的字符串地址才会刚好在0x7c30这个位置,我们可以做个实验来看看这个现象。
首先我们将链接地址指定为0x7c00,然后用反汇编查看代码,看看$BootMessage被编译成了多少,反汇编如下:
1 [root@localhost chapter1]# objdump -D -b binary -m i8086 bootsect.bin 2 3 bootsect.bin: 文件格式 binary 4 5 6 Disassembly of section .data: 7 8 00000000 <.data>: 9 0: 8c c8 mov %cs,%ax 10 2: 8e d8 mov %ax,%ds 11 4: 8e c0 mov %ax,%es 12 6: e8 02 00 call 0xb 13 9: eb fe jmp 0x9 14 b: bb 33 7c mov $0x7c33,%bx 15 e: 8b 07 mov (%bx),%ax 16 10: 89 c1 mov %ax,%cx 17 12: b8 23 7c mov $0x7c23,%ax 18 15: 89 c5 mov %ax,%bp 19 17: b8 01 13 mov $0x1301,%ax 20 1a: bb 0c 00 mov $0xc,%bx 21 1d: ba 00 00 mov $0x0,%dx 22 20: cd 10 int $0x10 23 22: c3 ret 24 23: 48 dec %ax 25 24: 65 6c gs insb (%dx),%es:(%di) 26 26: 6c insb (%dx),%es:(%di) 27 27: 6f outsw %ds:(%si),(%dx) 28 28: 2c 4f sub $0x4f,%al 29 2a: 53 push %bx 30 2b: 2c 57 sub $0x57,%al 31 2d: 6f outsw %ds:(%si),(%dx) 32 2e: 72 6c jb 0x9c 33 30: 64 21 00 and %ax,%fs:(%bx,%si) 34 33: 10 00 adc %al,(%bx,%si) 35 ... 36 1fd: 00 55 aa add %dl,-0x56(%di)
我们从上面可以看到,14行的BootMessage变成了0x7c23,我们看24行的数字0x48,正好是'H'的ASCII码,后面的就是字符串了,这段程序装到0x7c00地址处,那么字符串的地址就变成了0x7c23,显然,链接器根据我们提供的链接地址帮我们重新设置了这些标号的数值,但是如果我们修改了0x7c00,那么程序还能不能运行呢,对于mov %cs,%ax这类指令还是可以执行的,它们不包涵标号信息,还有就是相对调转指令等,这些指令在执行的时候不会直接将跳转的地址送入EIP,而是会根据信息计算一个相对于当前EIP的偏移量,这个时候这些指令就和它们所在的位置无关了(位置无关码),比如:
L:jmp L
如果我们在链接的时候指定链接地址为0x7c02的话会怎样?应该想到结果了吧,因为字符串大小也是一个标号,所以我们把程序改成下面这样:
1 1 .code16 #告诉编译器,这段代码将会在16位实模式下执行,如果在这里遇到对eax ebx等32位寄存器操作时,加上前缀0x66,因为16位和32位保护模式的译码方式不一样 2 2 .text 3 3 .globl _start #声明ld连接器入口_start 4 4 _start: 5 5 movw %cs,%ax #设置段寄存器 6 6 movw %ax,%ds 7 7 movw %ax,%es 8 8 call DispStr 9 9 1: 10 10 jmp 1b 11 11 12 12 DispStr: 13 13 movw $Strsize,%bx 14 14 movw $15,%ax #字符串大小 15 15 movw %ax,%cx 16 16 movw $BootMessage,%ax 17 17 movw %ax,%bp #es:bp 显示字符串地址 18 18 movw $0x1301,%ax #ah=0x13在teletype模式下显示字符串 al=1 显示属性在bl 19 19 movw $0x000c,%bx #显示颜色,背景色 20 20 movw $0x0000,%dx #dh:行 dl:列 21 21 int $0x10 22 22 ret 23 23 24 24 BootMessage: 25 25 .asciz "Hello,OS,World!" #以0结尾的字符串 26 26 27 27 Strsize: 28 28 .word .-BootMessage #'.'表示当前这一行的地址
29 29 30 30 .org 510 31 31 .word 0xaa55 #引导扇区标志
只是将movw (%bx),%ax 修改成了movw $15,%ax,编译运行如下图:
因为链接地址变成0x7c02,相应的BootMessage变成了0x7c25,这个地址正好是"llo,OS,World!",后面有两个奇怪的符号就是内存中未知的数据