裸机版的hello world
hello world程序绝对经典的让人落泪,这是很多人的第一个程序。这个程序在Brian Kernighan和Dennis M. Ritchie合著的《The C Programme Language》中使用而广泛流行。该程序也体现了两位作者心向世界的博大情怀。
本人编程也是从hello world程序开始的,但是我很多人写的hello world程序都需要库和操作系统的支持才能运行。今天我想来用C语言重新实现一个裸机版hello world程序,即不需要操作系统和库的支持,顺便纪念一下hello world程序和C语言。
首先看看实现裸机版的hello world程序所需要的工具:
-
LINUX操作系统
-
编译器:GCC、LD、nasm
-
文件编辑器
-
Make
-
GRUB引导器(安装LINUX时已经自带了)
下面我们从上向下完成hello world程序,首先来写好main函数,如下:
点击(此处)折叠或打开
-
void main()
-
{
-
printf("hello world!");
-
return;
- }
是不是很熟悉,这样的程序,我想很多人闭着眼一通盲码,都可以正确无误
好了,上面的代码依然是调用了printf函数输出“hello world!”字符串的,由于这裸机版的程序,所以不能调用库中的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