这两天无聊,随便找个写代码的练手。就就开始读这本书了。
day.2代码部分使用intel的汇编格式,nasm 的编译器。我在想如何将它尽可能的用C来写。
考虑到我只有linux下的gcc编译器,又不想装太多,所以就在linux下来试试吧。
编译的脚本流程:
gcc -fno-pie -m16 --static -c day2.c -o day2.o ld -f elf32 -m elf_i386 -fno-pie --script=boot.ld -shared --map --static day2.o -o day2
这里必须指定编译为16位的,这样才能在实模式下正确运行,如果是32位模式的话,调用call会出现call飞到其他位置,为了这个内容,我调试了一下午。
正常编译的可执行文件包含了很多其他用不到的内容,比如PE格式之类,对于loader而言,这些都是无法执行的,因此需要定制编译的脚本, 下面是脚本的内容:
ENTRY(_entry) OUTPUT_FORMAT(binary) SECTIONS { . = 0x7c00; . = ALIGN(4); .text : { *(.text) *(.text.*) } . = ALIGN(4); .data : { *(.data) *(.data.*) } . = ALIGN(4); .rodata : { *(.rodata) *(.rodata.*) } . = ALIGN(4); .sdata : { *(.sdata) *(.sdata.*) } . = ALIGN(4); .scommon : { *(.scommon) *(.scommon.*) } . = .; . = ALIGN(16); __got_start = .; .got : { *(.got) } __got_end = .; . = ALIGN(4); .sbss : { *(.sbss) *(.sbss.*) } .bss : { *(.bss) *(.bss.*) } . = ALIGN(16); .rel.dyn : { *(.rel.dyn) } .dynamic :{ *(.dynamic)} .interp : { *(.interp) } .dynstr : { *(.dynstr) KEEP(*(.dynstr))} .dynsym : { *(.dynsym) KEEP(*(.dynsym))} .bootflag1 0x1FE+0x7C00 : {*(.bootflag1)} }
其实大部分都没啥用,主要是定义入口函数为_entry, 输出格式为binary而非elf格式,代码的起始位置为0x7c00.
默认的nasm程序和gcc编译器生成的二进制文件,在制作启动盘的时候,都缺少启动磁盘和启动分区的标志。使用脚本的话,就方便很多了,可以直接将启动的标记写入到指定位置去。 也就是上面的bootflag1 和 bootflag2。因为day2中的数据非常的少,所以不考虑加载更多内容,因此将data的数据放到标记之前,这样启动的时候读取一个分区的数据就将要用到的 data放入到第一个分区中了,也因此, 这部分的代码+数据不能超过512-4个字节
下面是helloworld bootloader的c代码版本:
void _init(); void _fin(); __attribute__ ((naked)) void _entry() { __asm("jmp *%0": :"r"(_init)); } char msg[] = "\n\nhello, world\n"; void show_char(char c) { __asm("mov %0, %%al": :"r"(c)); __asm("mov $0x0e, %ah"); __asm("mov $15, %bx"); __asm("int $0x10"); } void _init() { int i =0; while(i<sizeof(msg)) { show_char(msg[i]); i++; } _fin(); } void _fin() { __asm("hlt"); } unsigned char __attribute__((section (".bootflag1"))) bootflag1[] __attribute__ ((aligned (1))) = {0x55,0xaa};//0xfff0aa55;
系统起始部分其实啥也没有,所以不可避免的还是要使用汇编相关的代码,只是使用C来组织下而已, 注意的是,这里的入口函数是_entry, 而非一般c语言的main函数,这在编译的自定义脚本中声明了。函数的naked属性声明,在没有使用变量的情况下,不会生成栈相关的处理。