helloworld程序绝对经典的让人落泪,这是很多人的第一个程序。这个程序在Brian Kernighan和Dennis M. Ritchie合著的《The C Programme Language》中使用而广泛流行。该程序也体现了两位作者心向世界的博大情怀。
本人编程也是从helloworld程序开始的,但是我很多人写的hello world程序都需要库和操作系统的支持才能运行。今天我想来用C语言重新实现一个裸机版hello world程序,即不需要操作系统和库的支持,顺便纪念一下hello world程序和C语言。
首先看看实现裸机版的helloworld程序所需要的工具:
LINUX操作系统
编译器:GCC、LD、nasm
文件编辑器
Make
GRUB引导器(安装LINUX时已经自带了)
下面我们从上向下完成hello world程序,首先来写好main函数,如下:
点击(此处)折叠或打开
- void main()
- {
- printf("hello world!");
- return;
- }
是不是很熟悉,这样的程序,我想很多人闭着眼一通盲码,都可以正确无误
好了,上面的代码依然是调用了printf函数输出“helloworld!”字符串的,由于这裸机版的程序,所以不能调用库中的printf函数,而是要自己亲自实现该函数。下面就去实现一个最简单的printf函数。如下:
点击(此处)折叠或打开
- void printf(char* fmt,...)
- {
- _strwrite(fmt);
- return;
- }
确实够简单了,没有像通常的printf函数处理多个参数,也没有对参数进行格式化处理,而是调用了_strwrite函数,下面接着实现_strwrite函数,如下:
点击(此处)折叠或打开
- void _strwrite(char* string)
- {
- char* p_strdst=(char*)(0xb8000);
- while(*string)
- {
- *p_strdst=*string++;
- p_strdst+=2;
- }
- return;
- }
_strwrite函数才是输出字符串的核心函数,它把字符串的每个字符,依次写入以0xb8000为开始地址的内存空间,这个内存空间默认映射是显卡的显存,并且我们知道计算机启动时显卡默认工作在字符模式下。对应于屏幕是每行80个字符,一共有25行。
可是有了这些代码就可以了吗,当然不行,因为是裸机,所以在调用C函数之前,还要初始化栈和CPU的一些寄存器,更为关键的是我们的程序要被GRUB引导加载,而这些动作用C语言又无法实现,这时我们的大汇编语言就该上场了,发挥它神奇的作用了,下面来用汇编语言写一段代码,如下:
点击(此处)折叠或打开
- MBT_HDR_FLAGS EQU 0x00010003
- MBT_HDR_MAGIC EQU 0x1BADB002
- MBT_HDR2_MAGIC EQU 0xe85250d6
- global _start
- extern main
- [section .start.text]
- [bits 32]
- _start:
- jmp _entry
- ALIGN 8
- mbt_hdr:
- dd MBT_HDR_MAGIC
- dd MBT_HDR_FLAGS
- dd -(MBT_HDR_MAGIC+MBT_HDR_FLAGS)
- dd mbt_hdr
- dd _start
- dd 0
- dd 0
- dd _entry
- ;以上是GRUB所需要的头
- ALIGN 8
- mbt2_hdr:
- DD MBT_HDR2_MAGIC
- DD 0
- DD mbt2_hdr_end - mbt2_hdr
- DD -(MBT_HDR2_MAGIC + 0 + (mbt2_hdr_end - mbt2_hdr))
- DW 2, 0
- DD 24
- DD mbt2_hdr
- DD _start
- DD 0
- DD 0
- DW 3, 0
- DD 12
- DD _entry
- DD 0
- DW 0, 0
- DD 8
- mbt2_hdr_end:
- ;以上是GRUB2所需要的头
- ;包含两个头是为了同时兼容GRUB、GRUB2
- ALIGN 8
- _entry:
- ;关中断
- cli
- ;关不可屏蔽中断
- in al, 0x70
- or al, 0x80
- out 0x70,al
- ;重新加载GDT
- lgdt [GDT_PTR]
- jmp dword 0x8 :_32bits_mode
- _32bits_mode:
- ;下面初始化C语言可能会用到的寄存器
- mov ax, 0x10
- mov ds, ax
- mov ss, ax
- mov es, ax
- mov fs, ax
- mov gs, ax
- xor eax,eax
- xor ebx,ebx
- xor ecx,ecx
- xor edx,edx
- xor edi,edi
- xor esi,esi
- xor ebp,ebp
- xor esp,esp
- ;初始化栈,C语言需要栈才能工作
- mov esp,0x9000
- ;调用C语言函数main
- call main
- ;让CPU停止执行指令
- halt_step:
- halt
- jmp halt_step
- GDT_START:
- knull_dsc: dq 0
- kcode_dsc: dq 0x00cf9e000000ffff
- kdata_dsc: dq 0x00cf92000000ffff
- k16cd_dsc: dq 0x00009e000000ffff
- k16da_dsc: dq 0x000092000000ffff
- GDT_END:
- GDT_PTR:
- GDTLEN dw GDT_END-GDT_START-1
- GDTBASE dd GDT_START
这段代码不必多说,上面的注释已经写的很好了,汇编程序代码也写好了,最后的工作就是编译链接程序了,编译还好说,但是链接就不能用通常链接应用程序的方法了,因为这时裸机程序,所以我们得写个链接脚本来控制链接过程,如下:
点击(此处)折叠或打开
- ENTRY(_start)
- OUTPUT_ARCH(i386)
- SECTIONS
- {
- . = 0x200000;
- __begin_start_text = .;
- .start.text : ALIGN(4) { *(.start.text) }
- __end_start_text = .;
- __begin_text = .;
- .text : ALIGN(4) { *(.text) }
- __end_text = .;
- __begin_data = .;
- .data : ALIGN(4) { *(.data) }
- __end_data = .;
- __begin_rodata = .;
- .rodata : ALIGN(4) { *(.rodata) *(.rodata.*) }
- __end_rodata = .;
- __begin_kstrtab = .;
- .kstrtab : ALIGN(4) { *(.kstrtab) }
- __end_kstrtab = .;
- __begin_bss = .;
- .bss : ALIGN(4) { *(.bss) }
- __end_bss = .;
- }
上面的链接脚本最关键的是告诉LD链接器,我们的程序从0x200000的内存地址开始运行。最后还要写个makefile控制编译、链接过程。如下:
点击(此处)折叠或打开
- MAKEFLAGS = -sR
- MKDIR = mkdir
- RMDIR = rmdir
- CP = cp
- CD = cd
- DD = dd
- RM = rm
- ASM= nasm
- CC= gcc
- LD= ld
- ASMBFLAGS= -f elf
- 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
- LDFLAGS= -s -static -T hello.lds -n --oformat binary
- PMHELLO_OBJS :=
- PMHELLO_OBJS += entry.o helkrlmain.o vgastr.o
- PMHELLO_BIN = pmhello.bin
- .PHONY : build clean all link
- all: clean build link
- clean:
- $(RM) -f *.o *.bin
- build: $(PMHELLO_OBJS)
- link: $(PMHELLO_BIN)
- $(PMHELLO_BIN): $(PMHELLO_OBJS)
- $(LD) $(LDFLAGS) -o $@ $(PMHELLO_OBJS)
- %.o : %.asm
- $(ASM) $(ASMBFLAGS) -o $@ $<
- %.o : %.c
- $(CC) $(CFLAGS) -o $@ $<
安装测试,在linux系统下则非常方便,因为linux系统已经安装好了GRUB2,默认情况下,只要把pmhello.bin文件复制到linux系统的/boot/目录下,同时修改/boot/grub/目录下的grub.cfg文件。如下图所示:
重启计算机就可以看到PMHELLO启动选项了……
该项目代码地址是:https://code.csdn.net/lmnos/pmhelloworld