helloworld程序绝对经典的让人落泪,这是很多人的第一个程序。这个程序在Brian KernighanDennis M. Ritchie合著的《The C Programme Language》中使用而广泛流行。该程序也体现了两位作者心向世界的博大情怀。

         本人编程也是从helloworld程序开始的,但是我很多人写的hello world程序都需要库和操作系统的支持才能运行。今天我想来用C语言重新实现一个裸机版hello world程序,即不需要操作系统和库的支持,顺便纪念一下hello world程序和C语言。

         首先看看实现裸机版的helloworld程序所需要的工具:

  1. LINUX操作系统

  2. 编译器:GCCLDnasm

  3. 文件编辑器

  4. Make

  5. GRUB引导器(安装LINUX时已经自带了)

下面我们从上向下完成hello world程序,首先来写好main函数,如下:

点击(此处)折叠或打开

  1. void main()
  2. {
  3.     printf("hello world!");
  4.     return;
  5. }


是不是很熟悉,这样的程序,我想很多人闭着眼一通盲码,都可以正确无误

好了,上面的代码依然是调用了printf函数输出“helloworld!”字符串的,由于这裸机版的程序,所以不能调用库中的printf函数,而是要自己亲自实现该函数。下面就去实现一个最简单的printf函数。如下:

点击(此处)折叠或打开

  1. void printf(char* fmt,...)
  2. {
  3.     _strwrite(fmt);
  4.     return;
  5. }


确实够简单了,没有像通常的printf函数处理多个参数,也没有对参数进行格式化处理,而是调用了_strwrite函数,下面接着实现_strwrite函数,如下:

点击(此处)折叠或打开

  1. void _strwrite(char* string)
  2. {
  3.     char* p_strdst=(char*)(0xb8000);
  4.     while(*string)
  5.     {
  6.         *p_strdst=*string++;
  7.         p_strdst+=2;
  8.     }
  9.     return;
  10. }


_strwrite函数才是输出字符串的核心函数,它把字符串的每个字符,依次写入以0xb8000为开始地址的内存空间,这个内存空间默认映射是显卡的显存,并且我们知道计算机启动时显卡默认工作在字符模式下。对应于屏幕是每行80个字符,一共有25行。

         可是有了这些代码就可以了吗,当然不行,因为是裸机,所以在调用C函数之前,还要初始化栈和CPU的一些寄存器,更为关键的是我们的程序要被GRUB引导加载,而这些动作用C语言又无法实现,这时我们的大汇编语言就该上场了,发挥它神奇的作用了,下面来用汇编语言写一段代码,如下:

点击(此处)折叠或打开

  1. MBT_HDR_FLAGS    EQU 0x00010003
  2. MBT_HDR_MAGIC    EQU 0x1BADB002
  3. MBT_HDR2_MAGIC    EQU 0xe85250d6
  4. global _start
  5. extern main
  6. [section .start.text]
  7. [bits 32]
  8. _start:
  9.     jmp _entry
  10. ALIGN 8
  11. mbt_hdr:
  12.     dd MBT_HDR_MAGIC
  13.     dd MBT_HDR_FLAGS
  14.     dd -(MBT_HDR_MAGIC+MBT_HDR_FLAGS)
  15.     dd mbt_hdr
  16.     dd _start
  17.     dd 0
  18.     dd 0
  19.     dd _entry

  20. ;以上是GRUB所需要的头
  21. ALIGN 8
  22. mbt2_hdr:
  23.     DD    MBT_HDR2_MAGIC
  24.     DD    0
  25.     DD    mbt2_hdr_end - mbt2_hdr
  26.     DD    -(MBT_HDR2_MAGIC + 0 + (mbt2_hdr_end - mbt2_hdr))
  27.     DW    2, 0
  28.     DD    24
  29.     DD    mbt2_hdr
  30.     DD    _start
  31.     DD    0
  32.     DD    0
  33.     DW    3, 0
  34.     DD    12
  35.     DD    _entry
  36.     DD 0
  37.     DW    0, 0
  38.     DD    8
  39. mbt2_hdr_end:
  40. ;以上是GRUB2所需要的头
  41. ;包含两个头是为了同时兼容GRUB、GRUB2

  42. ALIGN 8

  43. _entry:
  44.     ;关中断
  45.     cli
  46.     ;关不可屏蔽中断
  47.     in al, 0x70
  48.     or al, 0x80
  49.     out 0x70,al
  50.     ;重新加载GDT
  51.     lgdt [GDT_PTR]
  52.     jmp dword 0x8 :_32bits_mode

  53. _32bits_mode:
  54.     ;下面初始化C语言可能会用到的寄存器
  55.     mov ax, 0x10
  56.     mov ds, ax
  57.     mov ss, ax
  58.     mov es, ax
  59.     mov fs, ax
  60.     mov gs, ax
  61.     xor eax,eax
  62.     xor ebx,ebx
  63.     xor ecx,ecx
  64.     xor edx,edx
  65.     xor edi,edi
  66.     xor esi,esi
  67.     xor ebp,ebp
  68.     xor esp,esp
  69.     ;初始化栈,C语言需要栈才能工作
  70.     mov esp,0x9000
  71.     ;调用C语言函数main
  72.     call main
  73.     ;让CPU停止执行指令
  74. halt_step:
  75.     halt
  76.     jmp halt_step


  77. GDT_START:
  78. knull_dsc: dq 0
  79. kcode_dsc: dq 0x00cf9e000000ffff
  80. kdata_dsc: dq 0x00cf92000000ffff
  81. k16cd_dsc: dq 0x00009e000000ffff
  82. k16da_dsc: dq 0x000092000000ffff
  83. GDT_END:

  84. GDT_PTR:
  85. GDTLEN    dw GDT_END-GDT_START-1
  86. GDTBASE    dd GDT_START



这段代码不必多说,上面的注释已经写的很好了,汇编程序代码也写好了,最后的工作就是编译链接程序了,编译还好说,但是链接就不能用通常链接应用程序的方法了,因为这时裸机程序,所以我们得写个链接脚本来控制链接过程,如下:

点击(此处)折叠或打开

  1. ENTRY(_start)
  2. OUTPUT_ARCH(i386)
  3. SECTIONS
  4. {
  5. . = 0x200000;
  6. __begin_start_text = .;
  7. .start.text : ALIGN(4) { *(.start.text) }
  8. __end_start_text = .;
  9. __begin_text = .;
  10. .text : ALIGN(4) { *(.text) }
  11. __end_text = .;
  12. __begin_data = .;
  13. .data : ALIGN(4) { *(.data) }
  14. __end_data = .;
  15. __begin_rodata = .;
  16. .rodata : ALIGN(4) { *(.rodata) *(.rodata.*) }
  17. __end_rodata = .;
  18. __begin_kstrtab = .;
  19. .kstrtab : ALIGN(4) { *(.kstrtab) }
  20. __end_kstrtab = .;
  21. __begin_bss = .;
  22. .bss : ALIGN(4) { *(.bss) }
  23. __end_bss = .;
  24. }


上面的链接脚本最关键的是告诉LD链接器,我们的程序从0x200000的内存地址开始运行。最后还要写个makefile控制编译、链接过程。如下:

点击(此处)折叠或打开

  1. MAKEFLAGS = -sR
  2. MKDIR = mkdir
  3. RMDIR = rmdir
  4. CP = cp
  5. CD = cd
  6. DD = dd
  7. RM = rm
  8. ASM= nasm
  9. CC= gcc
  10. LD= ld
  11. ASMBFLAGS= -f elf
  12. CFLAGS= -c -Os -std=c99 -m32 -Wall -Wshadow -W -Wconversion -Wno-sign-conversion -fno-stack-protector -fomit-frame-pointer -fno-builtin -fno-common -ffreestanding -Wno-unused-parameter -Wunused-variable
  13. LDFLAGS= -s -static -T hello.lds -n --oformat binary
  14. PMHELLO_OBJS :=
  15. PMHELLO_OBJS += entry.o helkrlmain.o vgastr.o
  16. PMHELLO_BIN = pmhello.bin
  17. .PHONY : build clean all link
  18. all: clean build link
  19. clean:
  20. $(RM) -f *.o *.bin
  21. build: $(PMHELLO_OBJS)
  22. link: $(PMHELLO_BIN)
  23. $(PMHELLO_BIN): $(PMHELLO_OBJS)
  24. $(LD) $(LDFLAGS) -o $@ $(PMHELLO_OBJS)
  25. %.o : %.asm
  26. $(ASM) $(ASMBFLAGS) -o $@ $<
  27. %.o : %.c
  28. $(CC) $(CFLAGS) -o $@ $<


安装测试,在linux系统下则非常方便,因为linux系统已经安装好了GRUB2,默认情况下,只要把pmhello.bin文件复制到linux系统的/boot/目录下,同时修改/boot/grub/目录下的grub.cfg文件。如下图所示:

 裸机版的hello world-LMLPHP

重启计算机就可以看到PMHELLO启动选项了……

该项目代码地址是:https://code.csdn.net/lmnos/pmhelloworld

11-02 13:44