使用GCC生成无格式二进制文件(plain binary files)
使用C语言生成一个二进制文件
使用自己喜欢的文本编辑器写一个test.c:
int main() { } |
再使用如下命令编译:
gcc –c test.c ld –o test –Ttext 0x0 –e main test.o objcopy –R .note –R .comment –S –O binary test test.bin |
最后生成的二进制文件是test.bin,可以使用你喜欢的反汇编工具看看这个文件里到底是什么。我使用Linux下的objdump进行反汇编:
objdump –D –b binary –a i386 test.bin |
结果如下:
00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret |
其中第一列是指令的内存地址;第二列是指令的机器码;第三列是汇编指令。相信你的结果与此同。如果你的gcc与我的不一样,例如2.7.x版本的gcc,你的结果很可能会有所不同,缺少如下的四条指令,这是正常的,这两个版本的gcc所使用的堆栈框架不同(下面介绍的例子也会因为编译器版本的不同造成其结果有别):
3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp #堆栈对齐,以16Bytes为单位分配局部变量空间 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp |
上述代码都是32-bit代码,你需要在像Linux这样的 32-bit环境下运行,并且是保护模式。也可以只用下面的指令直接生成test.bin:
gcc –c test.c ld –Ttext 0x0 –e main --oformat binary –o test.bin test.o |
上面的test.c中只有一个函数,而且还只是个框架。其反汇编代码也没什么难理解的。
3. 编写带局部变量的程序
再创建一个新的test.c,看看gcc是如何处理局部变量的。
int main() { int i; i=0x12345678; } |
使用上述两种方法的人一种编译,生成test.bin。然后使用objdump进行反汇编:
00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c7 45 fc 78 56 34 12 movl $0x12345678,0xfffffffc(%ebp) 17: c9 leave 18: c3 ret |
与第一个例子相比,开头的六条指令和最后的两条指令完全相同,仅有一条指令不同。这条语句是给局部变量赋值,其空间的分配在前面已经进行了。在gcc中,堆栈中的局部变量空间按16字节为单位进行分配,而不是通常的1字节为单位。如果将
int i; i=0x12345678; 改为 int i=0x12345678; |
其结果没有区别。但是,如果是全局变量,就不一样了。
4. 编写带全局变量的程序
将test.c改为:
int i; int main() { i=0x12345678; } |
使用同样的方法编译,然后再进行反汇编:
00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c7 05 1c 10 00 00 78 movl $0x12345678,0x101c 17: 56 34 12 1a: c9 leave 1b: c3 ret |
我们定义的全局变量被放到了0x101c处,这是gcc默认以page-align对齐数据段的结果,此处的page与页式内存管理中的page没有关系。在使用ld链接时,使用-N参数可以关闭对齐效果。
00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c7 05 1c 00 00 00 78 movl $0x12345678,0x1c 17: 56 34 12 1a: c9 leave 1b: c3 ret |
正如我们看到的,数据段紧接着代码段。我们也可以明确的指定数据段的位置,试试下面的命令再进行编译:
gcc –c test.c ld –Ttext 0x0 –Tdata 0x1234 –e main –N --oformat binary –o test.bin test.o |
然后再使用objdump进行反汇编:
00000000 <.data>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c7 05 34 12 00 00 78 movl $0x12345678,0x1234 17: 56 34 12 1a: c9 leave 1b: c3 ret |
现在,我们定义的全局变量被放到0x1234处了。通过给ld指定-Tdata参数,可以自由的定义数据段的地址,如果不指定,数据段在代码段后。
再看看直接给全局变量进行初始化的情况。
const int I=0x12345678; int main() { } |
仍然使用上面的方法进行编译、链接、反汇编,其结果如下:
00000000 <.data>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret 12: 00 00 add %al,(%eax) 14: 78 56 js 0x6c 16: 34 12 xor $0x12,%al |
代码以4Bytes对齐,全局变量被直接存储在代码段之后的数据段,ld直接将常数放到了全局变量的位置,一步到位。
使用如下命令可以看到更多细节:
objdump –D test.o |
可以看到如下的结果:
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret Disassembly of section .data: Disassembly of section .rodata:
00000000 <i>: 0: 78 56 js 58 <main+0x58> 2: 34 12 xor $0x12,%al |
我们可以更清楚地看到,在.c文件中定义的全局常量被放在了只读的数据段中了。再看下面的一段代码:
int I=0x12345678; const int c=0x12345678; int main() { } |
还是使用上面的方法编译、链接、反汇编,可以到到如下结果:
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret Disassembly of section .data:
00000000 <i>: 0: 78 56 js 58 <main+0x58> 2: 34 12 xor $0x12,%al Disassembly of section .rodata:
00000000 <c>: 0: 78 56 js 58 <main+0x58> 2: 34 12 xor $0x12,%al |
可以看出,整数I被放在了普通的数据段中,常数c被放在了只读数据段中了。当使用全局变量(常量)时,ld会自动的使用合适的数据段存储他们。
5. 处理指针
使用如下代码来查看gcc处理指针变量的情况:
int main() { int I; int* p; p=&I; *p=0x12345678; } |
使用objdump查看生成的机器代码:
00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: 8d 45 fc lea 0xfffffffc(%ebp),%eax 13: 89 45 f8 mov %eax,0xfffffff8(%ebp) 16: 8b 45 f8 mov 0xfffffff8(%ebp),%eax 19: c7 00 78 56 34 12 movl $0x12345678,(%eax) 1f: c9 leave 20: c3 ret |
一开始,gcc已经为局部变量预分配了至少8Bytes的空间,并且使esp以16Bytes边界对齐,如果还需要额外的空间,gcc将按照16Bytes为单位进行分配,而不是其他编译器所使用的以1Byte为单位进行分配。变量I位于ebp-4,变量p位于ebp-8,lea指令将I的有效地址放入eax中,然后又被放入p中。最后,将0x12345678赋给p指向的变量I。
6. 关于函数调用
看如下代码:
void func(); int main() { func(); } void func() { } |
再看生成的二进制代码:
00000000 <.data>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: e8 03 00 00 00 call 0x18 15: c9 leave 16: c3 ret 17: 90 nop 18: 55 push %ebp 19: 89 e5 mov %esp,%ebp 1b: c9 leave 1c: c3 ret |
主函数main通过call指令调用了空函数func,该函数与main大同小异。为ld指定-Map开关输出map文件,可以得到更详细的信息。
.text 0x00000000 0x1d *(.text .stub .text.* .gnu.linkonce.t.*) .text 0x00000000 0x1d test.o 0x00000000 main 0x00000018 func |
第一列是段名,这里是.text;第二列是起始位置,第三列是段长度,最后一列是附加信息,如函数名、所出自的目标文件等。可以看到,.text段从0x0开始,长度为0x1d;函数func从0x18开始。
7. 函数的返回值
看下面的代码,主函数main返回一个整型值:
int main() { return 0x12345678; } |
所生成的二进制代码与其他编译器大同小异:
00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: b8 78 56 34 12 mov $0x12345678,%eax 15: c9 leave 16: c3 ret |
你已经看到了,gcc使用eax传递返回值。因为返回值就是eax寄存器的值,所以你可以隐含的返回,甚至什么都不返回。因为返回值保存在寄存器中,进行函数调用时,经常忽略返回值。例如,我们经常这样调用函数:
printf(…); |
该函数是有返回值的。如果函数返回的数据大于4Bytes,就不能再使用这种方法返回数据了。再看下面的例子:
typedef strUCt mydef{ int a,b,c,d; int array[10]; }mydef;
mydef func(); int main() { mydef d; d=func(); } mydef func() { mydef d; return d; } |
接着看反汇编的代码:
00000000 <.data>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 48 sub $0x48,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: 8d 45 b8 lea 0xffffffb8(%ebp),%eax 13: 83 ec 0c sub $0xc,%esp 16: 50 push %eax 17: e8 06 00 00 00 call 0x22 1c: 83 c4 0c add $0xc,%esp 1f: c9 leave 20: c3 ret 21: 90 nop 22: 55 push %ebp 23: 89 e5 mov %esp,%ebp 25: 57 push %edi 26: 56 push %esi 27: 83 ec 40 sub $0x40,%esp 2a: 8b 7d 08 mov 0x8(%ebp),%edi 2d: 8d 75 b8 lea 0xffffffb8(%ebp),%esi 30: fc cld 31: b8 0e 00 00 00 mov $0xe,%eax 36: 89 c1 mov %eax,%ecx 38: f3 a5 repz movsl %ds:(%esi),%es:(%edi) 3a: 8b 45 08 mov 0x8(%ebp),%eax 3d: 83 c4 40 add $0x40,%esp 40: 5e pop %esi 41: 5f pop %edi 42: c9 leave 43: c2 04 00 ret $0x4 |
我们自定义的结构为0x38Bytes,gcc为了保持堆栈的16Bytes对齐,分配了0x40Bytes的空间。函数func并没有参数,但是在调用时,却将变量d的指针传了进去。然后利用这个指针,使用指令movsl直接对d进行赋值。再看下面的例子:
typedef struct mydef{ int a,b,c,d; int array[10]; }mydef;
mydef func(); int main() { func(); } mydef func() { mydef d; return d; } |
再看反汇编的结果:
00000000 <.data>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 48 sub $0x48,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: 8d 45 b8 lea 0xffffffb8(%ebp),%eax 13: 83 ec 0c sub $0xc,%esp 16: 50 push %eax 17: e8 06 00 00 00 call 0x22 1c: 83 c4 0c add $0xc,%esp 1f: c9 leave 20: c3 ret 21: 90 nop 22: 55 push %ebp 23: 89 e5 mov %esp,%ebp 25: 57 push %edi 26: 56 push %esi 27: 83 ec 40 sub $0x40,%esp 2a: 8b 7d 08 mov 0x8(%ebp),%edi 2d: 8d 75 b8 lea 0xffffffb8(%ebp),%esi 30: fc cld 31: b8 0e 00 00 00 mov $0xe,%eax 36: 89 c1 mov %eax,%ecx 38: f3 a5 repz movsl %ds:(%esi),%es:(%edi) 3a: 8b 45 08 mov 0x8(%ebp),%eax 3d: 83 c4 40 add $0x40,%esp 40: 5e pop %esi 41: 5f pop %edi 42: c9 leave 43: c2 04 00 ret $0x4 |
可以说,与上面的结果一字不差!我们没有在main函数中声明变量存储func返回的结果,但是gcc替我们做了。它仍然为函数func传递了一个指针,并将结果传了出来,尽管我们对返回值不感兴趣,但编译器对我们的兴趣好像也没有兴趣,依然我行我素。(如果使用了优化选项,结果很可能有所相同)。
8. 给函数传递参数
gcc遵循一般的c语言标准,包括参数传递方式。看看下面的例子:
char res; char func(char a,char b); int main() { res=func(0x02,0x03); } char func(char a,char b) { return a+b; } |
再看看他的反汇编代码:
00000000 <.data>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: 83 ec 08 sub $0x8,%esp 13: 6a 03 push $0x3 15: 6a 02 push $0x2 17: e8 0a 00 00 00 call 0x26 1c: 83 c4 10 add $0x10,%esp 1f: a2 44 00 00 00 mov %al,0x44 24: c9 leave 25: c3 ret 26: 55 push %ebp 27: 89 e5 mov %esp,%ebp 29: 83 ec 04 sub $0x4,%esp 2c: 8b 45 08 mov 0x8(%ebp),%eax 2f: 8b 55 0c mov 0xc(%ebp),%edx 32: 88 45 ff mov %al,0xffffffff(%ebp) 35: 88 55 fe mov %dl,0xfffffffe(%ebp) 38: 8a 45 fe mov 0xfffffffe(%ebp),%al 3b: 02 45 ff add 0xffffffff(%ebp),%al 3e: 0f be c0 movsbl %al,%eax 41: c9 leave 42: c3 ret |
如果你精通汇编语言,看完这段代码,恐怕你已经口吐鲜血并晕倒在地了!gcc居然生成了这么啰嗦的代码!但是,我们还是先说说C语言的函数调用规范吧。
我们已经看到了,参数从右到左依次入栈。下面的说明全部以32Bytes代码为准,其规范具体可罗列以下几条:
l 调用者负责将参数压入堆栈,顺序为从右到左依次入栈。也就是左边的最后入栈。
l 调用者使用near call指令将控制权传给被调用者。
l 被调用者得到控制权,一般需要创建堆栈框架(这不是必需的,通常都是这么做)。首先,将ebp压入堆栈保存,再将esp放入ebp,使ebp成为访问参数的基址指针。
l 被调用者通过ebp访问参数。因为ebp已经先行压入堆栈,所以[ebp+4]就是被call指令自动压入堆栈的返回地址,显然,从[ebp+8]开始,就是参数。由于函数最左边的参数最后被压入堆栈,所以[ebp+8]就是该参数,其他参数以此类推。像printf这样的函数,具有个数不确定的参数,但是参数入栈顺序的规则,说明被调用者通过[ebp+8]就能够找到第一个参数,其他参数的类型和数目,则需要由第一个参数给出。
l 被调用者减小esp的值为堆栈中的临时变量分配空间,然后使用ebp和一个负的偏移访问。
l 被调用者使用al,ax,eax返回大小不同的值。浮点数可以通过ST0寄存器返回。
l 被调用者完成处理后,使用事先建立的堆栈框架,恢复esp,sbp的值,并使用ret指令返回调用者。
l 调用者重新得到控制权,通过给esp加上一个立即数清空堆栈(尽量不要使用多次pop指令清空堆栈)。如果因为使用了错误的函数原型通过堆栈多传递了或者少传递了参数,调用者仍然能够将堆栈恢复到正确的状态,因为调用者知道自己向堆栈压了几个字节的数据。
结合C语言的函数调用规则,上面的代码不难理解。
从80386开始,push指令的操作数可以是8-bit,16-bit,32-bit,但是C语言统统按32-bit整型数处理,被调用者也按32-bit进行处理。这一点很重要,特别是汇编语言和C语言混合编程时。
9. 基本的数据类型间的转换
gcc处理三类基本数据类型:
l signed char , unsigned char , 1 Byte
l signed short , unsigned short , 2 Bytes
l signed int , unsigned int , 4 Bytes
各种数据类型间的转换,遵循一般C语言的规则,具体可以参考IA-32的标准。这里只举一例说明:
int main() { char ch=’a’; int x=2; int y=-4; } |
使用同样的方法进行编译及反汇编:
00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 18 sub $0x18,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c6 45 ff 61 moVB $0x61,0xffffffff(%ebp) 14: c7 45 f8 02 00 00 00 movl $0x2,0xfffffff8(%ebp) 1b: c7 45 f4 fc ff ff ff movl $0xfffffffc,0xfffffff4(%ebp) 22: c9 leave 23: c3 ret |
10. gcc编译代码的基本运行环境
这一部分,我查了很多的文档,都没有这方面的介绍。又请教了很多的高手,大致情况如下,我实在无法保证这里所说的都是正确的,并且将来也是正确的,仅供参考:
l 32-bit的保护模式下运行。
l 段寄存器CS,ds,es,fs,gs,ss必须指向同一段内存区域。
l 没有初始化的全局变量被放在BSS的段内,该区域在代码段之后。但是,如果你生成的文件是二进制文件,BSS段不是该文件的一部分,你需要自己小心使用。初始化的全局变量在DATA段内,它是二进制文件的一部分,并且位于代码段之后。被声明为const的全局变量被放在RODATA段内,它也是二进制文件的一部分,并放在代码段之后。
l 确保堆栈没有溢出,小心代码段和全局数据不要被破坏。
我也查了Intel提供的帮助文档“Intel Architecture Software Developer’s Manual”,一共有三卷之多!参考了其中关于内存组织(Volume 1:Memory Organization)中的说法(建议你去好好研究)。总之,使cs,ds,ss总是指向同一内存区域应该可以使代码正确运行。如果运行环境不是这样,我就不知道结果了。
11. 访问外部的全局变量
看看在非C语言程序中如何访问C语言程序中的全局变量。如果你想使用其他程序加载C程序,例如汇编语言写的程序,这部分很有用,特别是在核心开发时经常用到。
int myVal=0x5; int main() { } |
编译这段代码:
gcc –c test.c ld –Ttext 0x0 –e main –N –oformat binary –Map memmap.txt –o test.bin test.o objdump –D –b binrary –m i386 test.bin |
得到如下结果:
00000000 <.data>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret 12: 00 00 add %al,(%eax) 14: 05 .byte 0x5 15: 00 00 add %al,(%eax) |
全局变量myVal存储在0x14。刚才已经使用-Map开关使ld生成了内存映像文件memmap.txt,应该能够找到:
.data 0x00000014 0x4 *(.data .data.* .gnu.linkonce.d.*) .data 0x00000014 0x4 test.o 0x00000014 myVal |
说明myVal位于test.o模块的0x00000014位置。使用地址作为偏移量,就可以直接在其他语言中访问myVal变量了。另为也可以通过memmap.txt查到BSS段的大小:
cat memmap.txt grep ‘/.bss’ grep ‘0x’ sed ‘s/.*0x/0x/’ |
本例子,BSS的大小是0x0。
无法直接访问C程序中的使用static修饰的全局变量。因为这样的变量是静态的,map文件中没有列出他们的地址。也许你可以使用其他办法做到,但是,最好不要这样做。
12. 生成其他格式的二进制文件的选项
生成不同格式的二进制文件是一件相当麻烦的事。它需要使用很多不常使用的选项,并且有些在man的帮助信息中没有被列出。
首先是gcc的选项:-nostdinc。很显然,使用该选项后,gcc就不搜索默认的include路径了,通常是/usr/include。如果需要使用的自定义的头文件,可以使用-I选项添加搜索路径。
然后是ld的选项。第一个是-nostdlib,就是忽略标准库。如果需要,可以使用-L选项指定库的搜索路径。第二个是-Ttext,就是指定代码段的地址,如果没有继续指定其他段的地址,则他们将自动的一次被放在代码段之后。第三个是-e,就是指定代码的入口地址,默认的是_start,如果代码不是以其开头,就应该指定入口点。第四个是—oformat binary,就是输出的文件是原始的二进制文件,而是如文件可以使系统支持的任何文件。但是,中间模块文件不能是原始的二进制文件,因为还需要很多符号和重定位信息。可以使用—iformat选项指定输入文件的格式,但通常很少使用。第五个是-static,如果使用了其他库,用该使用静态链接方式,除非你的程序支持动态链接。
另外还有代码指示伪指令。汇编器可以编译16-bit代码,也可以编译32-bit代码。但是,gcc总是生成32-bit的汇编代码。通过在C代码中使用asm()伪指令可以让gcc生成16-bit汇编代码。
第一个是.code16,即生成在16-bit段中运行的16-bit代码;
第二个是.code32,即生成在32-bit段中运行的32-bit代码,默认情况下gcc总是这么做;
第三个是.code16gcc,gcc将根据需要决定生成在16-bit段下运行的16-bit或32-bit代码。GAS将会加上必要的前缀,指示32-bit的指令或寄存器等。这个选项是很有用的,它允许我们使用C语言写在16-bit环境下运行的代码,不论是实模式还是保护模式。
现在可以在一个C模块中既有16-bit代码,又有32-bit代码,但是此时需要注意不同部分代码的地址空间问题。
例如,我们想使用gcc生成在DOS下运行的.com程序和启动引导程序。
首先,DOS中的.com文件是在实模式下运行的原始的二进制文件,其起始地址为0x100。要使用gcc生成.com文件,在每一个.c文件的开头加上如下伪指令:
__asm__(“code16gcc/n”); |
如果需要引用其他库文件,则这些库文件也需要按这种方式生成。在链接时,加上如下选项:
-Ttext 0x100 –static –oformat binary |
如果程序中包含嵌入的汇编代码,需要将其转换为AT&T格式。
如果要写引导程序,只需要在链接时使用0x7C00代替0x100!另外,最终生成的二进制代码必须小于446个字节!
13. 参考资料
l Intel Architecture Software Developer’s Manual
l Manual Pages in Linux
l Redhat GNUPro Toolkit