操作系统内核Hack:(三)引导程序制作
来源:互联网 发布:linux samba服务器搭建 编辑:程序博客网 时间:2024/05/18 23:52
操作系统内核Hack:(三)引导程序制作
关于本文涉及到的完整源码请参考MiniOS的v1_bootloader分支。
1.制作方法
现在我们已经了解了关于BootLoader的一切知识,让我们开始动手做一个BootLoader吧!但真正开始之前,我们还要做出一个选择,在之前的讨论中我们曾说过,有两种学习和制作引导程序和操作系统内核的路线:1)《Orange’s:一个操作系统的实现》书中的路线;2)Linux 0.11的路线。
1.1 两种实现思路
具体来说,第一种路线就是将BootLoader和Kernel都放到预先格式化成FAT的软盘上,BootLoader通过FAT和ELF的知识定位并加载Kernel,借助FreeDOS启动或调试我们的操作系统。并非对FAT和DOS心存偏见,毕竟现在的确很多汇编语言书籍仍然以DOS作为学习平台,但我个人更喜欢第二种路线。在引导过程,我们不依赖任何文件系统,在裸扇区中加载运行第二阶段BootLoader和Kernel。进入Kernel之后,我们再挂载一个根文件系统。为何要这样做而不跟着《Orange’s》作者的思路走呢?因为我们毕竟主要学习和模仿的是*nix族的操作系统,既然如此,为何不纯粹一些?直接循着当年Linus在Linux 0.11中的思路,这才算是一次酣畅淋漓的操作系统内核的Hack之旅!
经过了本系列前两回对实验环境和底层编程基础知识的准备,加上刚才对实现思路的分析,现在我们就参考《Linux内核完全注释》中Linux 0.11的思路,借鉴《Orange’s:一个操作系统的实现》中NASM的汇编代码,“双剑合璧,威力无穷”。让我们站在前人的肩膀上,一起动手来实现一个属于我们自己的操作系统BootLoader!
1.2 Linux 0.11参考
既然要借鉴思路,我们就先看一下Linux 0.11这个成品是什么样子。因为《Linux内核完全注释》中提供的源代码链接要做很多手动修改和准备工作才能使用,而网上有很多热心的网友分享了开箱即用的0.11源码安装包。
1.2.1 编译内核
cdai@cdai ~/Source $ git clone https://github.com/yuanxinyu/Linux-0.11cdai@cdai ~/Source $ cd Linux-0.11cdai@cdai-Vostro-1450 ~/Source/Linux-0.11 $ make help<<<<This is the basic help info of linux-0.11>>>Usage: make --generate a kernel floppy Image with a fs on hda1 make start -- start the kernel in qemu make debug -- debug the kernel in qemu & gdb at port 1234 make disk -- generate a kernel Image & copy it to floppy make cscope -- genereate the cscope index databases make tags -- generate the tag file make cg -- generate callgraph of the system architecture make clean -- clean the object files make distclean -- only keep the source code filesNote!: * You need to install the following basic tools: ubuntu|debian, qemu|bochs, ctags, cscope, calltree, graphviz vim-full, build-essential, hex, dd, gcc 4.3.2... * Becarefull to change the compiling options, which will heavily influence the compiling procedure and running result. ...<<<Be Happy To Play With It :-)>>>cdai@cdai ~/Source/Linux-0.11 $ sudo apt-get install -y ctags cscope graphviz qemucdai@cdai ~/Source/Linux-0.11 $ make
1.2.2 下载硬盘IMG
从OldLinux下载硬盘镜像hdc-0.11.img,大小为127MB。将其拷贝到Linux0.11目录下后,就可以执行make start
用qemu模拟Linux 0.11的运行环境了。
cdai@cdai ~/Source/Linux-0.11 $ tree├── boot├── fs├── hdc-0.11.img├── Image├── include├── init├── kernel├── lib├── Makefile├── Makefile.header├── mm├── README.md├── readme.old├── System.map└── tools8 directories, 10 filescdai@cdai ~/Source/Linux-0.11 $ make start
1.2.3 安装运行
启动时,系统会从软盘引导,并挂载根文件系统:
SeaBIOS (version 1.7.4-20140219_122725-roseapple)iPXE (http://ipxe.org) 00:0.3.0 C900 PCI2.10 PnP PMM+00FC1110+00F21110 C900Booting from Floppy...Loading system...Partition table ok.43012/62000 free blocks19719/20666 free inodes3446 buffers = 3528704 bytes buffer spaceFree mem: 12574720 bytes Ok.[/usr/root]# lsREADME hello mtools.howto shoelace.tar.Zgcclib140 hello.c shoe
2.文件目录组织
文件目录组织是按照打包后的模块划分,一级目录分为boot1、boot2、system三个文件夹,tools中放的则是打包工具类。对应《Orange’s》代码,boot1/bootsect.s相当于boot.asm,boot2/setup.s相当于loader.asm。
cdai@cdai ~/Workspace/syspace/1-assembly $ tree -d minios/minios/├── boot1│ └── bootsect.s├── boot2│ └── setup.s├── Makefile├── system│ ├── fs│ ├── include│ ├── init│ ├── kernel│ ├── lib│ └── mm└── tools └── build.c10 directories, 4 files
3.实用工具
3.1 构建工具
3.1.1 Makefile
首先,Makefile份内的工作就是编译出bootsect、setup、system、build。之后,调用build处理前三者,最终产生拼接好的可引导Image镜像文件。在此过程中,Makefile还有一些小任务要完成。例如利用临时汇编文件tmp.s将system模块的大小拼接到bootsect.asm的头部,加载system的代码中会用到它。
################## Macro & Rule#################AS = nasmASFLAGS = -f elfCC = gccCFLAGS = -Wall -OLD = ld# -Ttext org -e entry -s(omit all symbol info)# -x(discard all local symbols) -M(print memory map)LDFLAGS = -Ttext 0 -e main --oformat binary -s -x -M%.o: %.c $(CC) $(CFLAGS) -c -o $@ $<#%.o: %.asm# $(AS) $(ASFLAGS) -o $@ $<################## Default#################all: ImageImage: boot1/bootsect boot2/setup system/system tools/build tools/build boot1/bootsect boot2/setup system/system > a.bin dd if=a.bin of=Image bs=8192 conv=notrunc rm -f a.bintools/build: tools/build.c $(CC) $(CFLAGS) -o $@ $<# SYSSIZE= number of clicks (16 bytes) to be loadedboot1/bootsect: boot1/bootsect.asm system/system (echo -n "SYSSIZE equ (";ls -l system/system | grep system \ | cut -d " " -f 5 | tr '\012' ' '; echo "+ 15 ) / 16") > tmp.asm cat $< >> tmp.asm $(AS) -o $@ tmp.asm rm -f tmp.asmboot2/setup: boot2/setup.asm $(AS) -o $@ $<system/system: system/init/main.o system/init/myprint.o $(LD) $(LDFLAGS) \ system/init/main.o \ system/init/myprint.o \ -o $@ > System.mapsystem/init/main.o: system/init/main.csystem/init/myprint.o: system/init/myprint.asm $(AS) $(ASFLAGS) -o $@ $<################## Create floppy#################disk: bximage -q -fd -size=1.44 Image.PHONY: disk################## Start Vm#################start: Image bochs -q -f bochsrcqemu: Image qemu-system-x86_64 -m 16M -boot a -fda Image.PHONY: start################## GitHub#################commit: git add . git commit -m "$(MSG)"################## Clean#################clean: rm -f boot1/bootsect boot2/setup system/system tools/build rm -f system/**/*.o rm -f a.bin tmp.asm System.map.PHONY: clean
3.1.2 build.c
build.c主要完成一些不便于或无法在Makefile中实现的操作:
- 文件有效性的检查:例如bootsect末尾的0x55AA签名
- 文件填充:例如对不足四个扇区大小的setup进行填充
- 文件内容解析:例如解析出a.out或ELF格式的system,将其中二进制代码部分提取出来
- 拼接:将bootsect、setup、system三者拼接成Image可引导镜像
#include <stdio.h> /* fprintf */#include <string.h>#include <stdlib.h> /* exit */#include <sys/types.h> /* unistd.h needs this */#include <unistd.h> /* read/write */#include <fcntl.h>#define BUFFER_SIZE 1024#define SETUP_SECTS 4 /* max number of sectors of setup */#define STRINGIFY(x) #x /* cat string */#define GCC_HEADER 1024 /* GCC header length */#define SYS_SIZE 0x2000 /* max system length (SYS_SIZE*16=128KB) */void die(const char *str){ fprintf(stderr, "%s\n", str); exit(1);}void usage(){ die("Usage: build bootsect setup system [> image]");}void copy_bootsect(char const *filename, char *buf){ int c, fd; if ((fd = open(filename, O_RDONLY, 0)) < 0) die("Unable to open 'bootsect'"); for (c = 0; c < BUFFER_SIZE; c++) buf[c] = 0; c = read(fd, buf, BUFFER_SIZE); close(fd); fprintf(stderr, "'bootsect' is %d bytes.\n", c); if (c != 512) die("'bootsect' must be exactly 512 bytes"); if ((*(unsigned short *)(buf + 510)) != 0xAA55) die("'bootsect' hasn't got boot flag (0xAA55)"); c = write(1, buf, 512); if (c != 512) die("Write call failed");}void copy_setup(char const *filename, char *buf){ int c, i, fd; if ((fd = open(filename, O_RDONLY, 0)) < 0) die("Unable to open 'setup'"); for (i = 0; (c = read(fd, buf, BUFFER_SIZE)) > 0; i += c) if (write(1, buf, c) != c) die("Write call failed"); close(fd); fprintf(stderr, "'setup' is %d bytes\n", i); if (i > SETUP_SECTS * 512) die("'setup' exceeds " STRINGIFY(SETUP_SECTS) " sectors"); // Fill '\0' if smaller than max bytes for (c = 0; c < BUFFER_SIZE; c++) buf[c] = 0; while(i < SETUP_SECTS * 512) { c = SETUP_SECTS * 512 - i; if (c > BUFFER_SIZE) c = BUFFER_SIZE; if (write(1, buf, c) != c) die("Write call failed"); i += c; }}void copy_system(char const *filename, char *buf){ int c, i, fd; if ((fd = open(filename, O_RDONLY, 0)) < 0) die("Unable to open 'system'"); /*if (read(fd, buf, GCC_HEADER) != GCC_HEADER) die("Unable to read header of 'system'"); if (((long *) buf)[5] != 0) die("Non-GCC header of 'system'");*/ for (i = 0; (c = read(fd, buf, BUFFER_SIZE)) > 0; i += c) if (write(1, buf, c) != c) die("Write call failed"); close(fd); fprintf(stderr, "'system' is %d bytes\n", i); if (i > SYS_SIZE * 16) die("'system' is too big");}int main(int argc, char const *argv[]){ char buf[BUFFER_SIZE]; if (argc != 4) usage(); copy_bootsect(argv[1], buf); copy_setup(argv[2], buf); copy_system(argv[3], buf); return 0;}
编写build.c中可能会碰到一些C语言的小问题:例如如何字符串拼接,sizeof(array)和sizeof(pointer)陷阱等,这里就不细说了,回头总结C语言开发问题时才一并总结吧。
3.2 调试工具
3.2.1 BIN反汇编
由于BIN文件是Raw Binary,没有文件头及其他附加信息,所以直接运行objdump是会报错的。用objdump -D -b binary -m i386
,其中-D表示对整个文件反汇编,-b表示二进制,-m表示指令集架构。objdump的确可以反汇编出一些指令,但不够准确,尤其是16位操作数的处理。最好的办法就是用NASM自带的反汇编工具ndisasm,-o指定起始地址,-e指定开头跳过的字节数,-k指定不反汇编的位置,例如不用-o参数时-k 0x1FE,2
会跳过0x55AA签名。与objdump对比一下,看!是不是比objdump好多了!
cdai@cdai ~/Workspace/syspace/1-assembly/minios $ objdump -D -b binary -m i386 Image Image: file format binaryDisassembly of section .data:00000000 <.data>: 0: b8 c0 07 8e d8 mov $0xd88e07c0,%eax 5: b8 00 90 8e c0 mov $0xc08e9000,%eax a: b9 00 01 31 f6 mov $0xf6310100,%ecx f: 31 ff xor %edi,%edi 11: f3 a5 rep movsl %ds:(%esi),%es:(%edi) 13: ea 18 00 00 90 8c c8 ljmp $0xc88c,$0x90000018 1a: 8e d8 mov %eax,%ds ...cdai@cdai ~/Workspace/syspace/1-assembly/minios $ ndisasm -o 0x7c00 Image00007C00 B8C007 mov ax,0x7c000007C03 8ED8 mov ds,ax00007C05 B80090 mov ax,0x900000007C08 8EC0 mov es,ax00007C0A B90001 mov cx,0x10000007C0D 31F6 xor si,si00007C0F 31FF xor di,di00007C11 F3A5 rep movsw00007C13 EA18000090 jmp word 0x9000:0x1800007C18 8CC8 mov ax,cs00007C1A 8ED8 mov ds,ax
3.2.2 Bochs调试
因为之前在《C实战:强大的程序调试工具GDB》中已经学习并总结过GDB调试器的使用,所以这里就简单说一下Bochs对应的命令:
- 执行:b加断点,c继续执行,s和p分别是step in/step next单步调试
- 查看寄存器:r查看通用寄存器,sreg查看段寄存器
- 查看内存:x查看线性地址,xp查看物理地址。例如
xp /40bx 0x7c00
表示以十六进制(x)字节(b)的形式,查看0x7c00位置长度为40字节的数据 - 反汇编:u反汇编一段内存中的代码。例如
x 0x7c00 0x7c10
<bochs:5> xp /40bx 0x7c00[bochs]:0x00007c00 <bogus+ 0>: 0xb8 0xc0 0x07 0x8e 0xd8 0xb8 0x00 0x900x00007c08 <bogus+ 8>: 0x8e 0xc0 0xb9 0x00 0x01 0x31 0xf6 0x310x00007c10 <bogus+ 16>: 0xff 0xf3 0xa5 0xea 0x18 0x00 0x00 0x900x00007c18 <bogus+ 24>: 0x8c 0xc8 0x8e 0xd8 0x8e 0xc0 0x8e 0xd00x00007c20 <bogus+ 32>: 0xbc 0x00 0xff 0xba 0x00 0x00 0xb9 0x02<bochs:6> u 0x7c00 0x7c1000007c00: ( ): mov ax, 0x07c0 ; b8c00700007c03: ( ): mov ds, ax ; 8ed800007c05: ( ): mov ax, 0x9000 ; b8009000007c08: ( ): mov es, ax ; 8ec000007c0a: ( ): mov cx, 0x0100 ; b9000100007c0d: ( ): xor si, si ; 31f600007c0f: ( ): xor di, di ; 31ff
4.源码分析
4.1 第一阶段引导:bootsect.asm
因为受限于引导扇区的512字节,所以bootsect的工作很简单,首先就是将自己挪动到90000h高地址处,“明哲保身”避免被后面要加载的模块覆盖掉。然后打印提示信息”Loading system…”并加载setup和system后,就跳转到setup继续执行。为了简化,我们暂时没有根据make时拼接到bootsect.asm头部的SYSSIZE去加载system,而只是加载了第6个扇区。
; ############################; Constants; ############################SETUPLEN equ 4BOOTSEG equ 0x07c0INITSEG equ 0x9000SETUPSEG equ 0x9020SYSSEG equ 0x1000; ############################; Booting Process; ############################ org 07c00h; 1) Move bootsect to 0x90000 mov ax, BOOTSEG mov ds, ax mov ax, INITSEG mov es, ax mov cx, 256 ; cx = counter xor si, si ; ds:si = source xor di, di ; es:di = target rep movsw ; move word by word jmp INITSEG:go-$$ ; far jump!go: mov ax, cs ; re-init registers mov ds, ax mov es, ax mov ss, ax mov sp, 0xff00 ; arbitrary value >> 512; 2) Load setup module at 0x90200load_setup: mov dx, 0000h ; dx = driver(dh)/head(dl) mov cx, 0002h ; cx = track(ch)/sector(cl) mov bx, 0200h ; es:bx = target(es=9000h,bx=90200-90000) mov ah, 02h ; ah = service id(ah=02 means read) mov al, SETUPLEN ; al = number of sectors to read(al) int 13h jnc ok_load_setup xor dl, dl ; reset floppy and retry if failed xor ah, ah int 13h jmp load_setup ; 3) Display loading messageok_load_setup: mov ah, 03h ; read cursur position xor bh, bh int 10h mov bp, msg1-$$ ; es:bp = message start address mov cx, 21 ; cx = message length mov ax, 1301h mov bx, 0007h ; bx = page no.(bh=0 page-0) ; attribute(bl=7 white fg and black bg) mov dl, 0h int 10h; 4) Load system module at 0x10000load_system: mov ax, SYSSEG mov es, ax mov dx, 0000h ; dx = driver(dh)/head(dl) mov cx, 0006h ; cx = track(ch)/sector(cl) mov bx, 00h ; es:bx = target(es=1000h,bx=0) mov ah, 02h ; ah = service id(ah=02 means read) mov al, 01h ; al = number of sectors to read(al) int 13h jnc ok_load_system xor dl, dl ; reset floppy and retry if failed xor ah, ah int 13h jmp load_system; 5) Jump to setupok_load_system: jmp SETUPSEG:0h; ############################; Message; ############################msg1: db 13,10 ; CRLF db "Loading system..." db 13,10 ; CRLFtimes 510-($-$$) db 0 dw 0xaa55
为什么在bootsect默认保留一些字节?因为只有bootsect的长度是确定的512字节,在末尾保留一些“全局变量”的话,其他模块很容易就能访问到。
4.2 第二阶段引导:setup.asm
setup.asm看似很长,其实都是“见怪不怪”的代码了。开头部分的段描述符的宏定义和属性的常量定义,后面就是进入保护模式的常规代码了,就是这么简单。为了简化,跳转system前直接将ss置为00000h地址处。尽管如此,这段代码却耗费了我大量时间去调错,那究竟了哪里容易出问题呢?
; ############################; Constants & Macro; ############################; 描述符类型DA_32 equ 4000h ; 32 位段DA_LIMIT_4K equ 8000h ; 段界限粒度为 4K 字节; 存储段描述符类型DA_DR equ 90h ; 存在的只读数据段类型值DA_DRW equ 92h ; 存在的可读写数据段属性值DA_C equ 98h ; 存在的只执行代码段属性值DA_CR equ 9Ah ; 存在的可执行可读代码段属性值; Descriptor macro%macro Descriptor 3 dw %2 & 0FFFFh ; Limit 1 dw %1 & 0FFFFh ; Base addr 1 db (%1 >> 16) & 0FFh ; Base addr 2 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; Attr 1 + Limit 2 + Attr 2 db (%1 >> 24) & 0FFh ; Base addr 3%endmacro; ############################; Booting Process; ############################[SECTION .s16][BITS 16]LABEL_BEGIN:; 1) Enter protection mode mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0100h ; 1.1) Init descriptor xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_SEG_CODE32 mov word [LABEL_DESC_CODE32 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE32 + 4], al mov byte [LABEL_DESC_CODE32 + 7], ah ; 1.2) Load gdt to gdtr xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_GDT ; eax <- gdt base addr mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt base addr lgdt [GdtPtr] ; 1.3) Disable interrupt cli ; 1.4) Enable A20 addr line in al, 92h or al, 00000010b out 92h, al ; 1.5) Set PE in cr0 mov eax, cr0 or eax, 1 mov cr0, eax ; 1.6) Jump to protective mode! jmp dword SelectorCode32:0 ; SelectorCode32 (LABEL_SEG_CODE32:0)[SECTION .s32]ALIGN 32[BITS 32]LABEL_SEG_CODE32:; 2) Jump to system mov ax, SelectorData mov ds, ax mov es, ax mov ss, ax mov esp, 1000h jmp SelectorSystem:0SegCode32Len equ $ - LABEL_SEG_CODE32[SECTION .gdt]; Base Addr, Limit, AttributeLABEL_GDT: Descriptor 0, 0, 0LABEL_DESC_CODE32: Descriptor 0, SegCode32Len, DA_CR | DA_32 | DA_LIMIT_4KLABEL_DESC_SYSTEM: Descriptor 10000h, 0ffffh, DA_CR | DA_32 | DA_LIMIT_4KLABEL_DESC_DATA: Descriptor 0, 0ffffh, DA_DRW | DA_32 | DA_LIMIT_4KLABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRWGdtLen equ $ - LABEL_GDTGdtPtr dw GdtLen - 1 ; GDT limit dd 0 ; GDT base addrSelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDTSelectorSystem equ LABEL_DESC_SYSTEM - LABEL_GDTSelectorData equ LABEL_DESC_DATA - LABEL_GDTSelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
此外,SegCode32Len equ $ - LABEL_SEG_CODE32
一定要放到32位代码段的末尾,因为这个长度会设置到32位代码段的描述符中的Limit字段上。如果执行长度超过Limit的话,Bochs同样会没有任何提示的崩溃。
4.3 内核空壳:main.c
一路磕磕绊绊,我们终于进入了32位保护模式下的内核代码。现阶段我们在内核中还做不了什么,但这一路辛苦走下来,到这里还是应该尝点甜头儿的。那我们就在内核代码中用C程序调用汇编代码,输出一段话吧。我们先看源代码,后面会详细解释一下为什么要混合使用C和汇编,以及源代码中的细节问题。
4.3.1 源代码
从C程序调用汇编代码其实非常简单,我们并不用关注myprint()到底是什么语言实现的,只要这个函数原型使我们产生的代码能够与myprint()的实现一起工作就可以了。此外,编译myprint.asm的时候要注意不能编译成默认的BIN文件,而要编译为ELF格式产生可重定位的相关信息,这样链接器才能将其与main.o正确地链接到一起。注意参数msg的类型要声明为const char *msg,避免编译时的警告。
// main.cvoid myprint(const char *msg, int len);int main(int argc, char const *argv[]){ myprint("Hello, MiniOS!\n", 15); return 0;}
我们在.loop循环外初始化循环中要用到的变量:
- gs:显存映射空间的选择符,值32对应我们在setup.asm中建立的GDT中的SelectorVideo
- ah:字符串的属性,我们一会儿在Bochs中打印内存时能看到它
- ebx:循环中要递增的列值
- ecx:循环中要递减的循环次数计数器,对应C程序传入的参数len
- edx:循环中要递增的字符串首地址,对应C程序传入的参数msg。esp相当于一个二级指针,[esp+4]得到的只是msg,[msg]得到才是第一个字符’H’。所以说,函数原型(char *msg)是与汇编代码([esp+4])一一对应的,从此处也能一瞥C语言的灵活和强大。
; myprint.asm[section .data][section .text]global myprintmyprint: mov ax, 32 ; SelectorVideo mov gs, ax mov ah, 0Ch ; attr mov ebx, 0 ; col mov ecx, [esp+8] ; msg len mov edx, [esp+4] ; msg addr.loop: ;(80 * row + col) * 2 mov edi, ebx add edi, (80 * 20) imul edi, 2 mov al, byte [edx] mov [gs:edi], ax inc ebx dec ecx inc edx cmp ecx, 0h jne .loop jmp $
4.3.2 反汇编
Makefile中已经有了三个反汇编的target,分别利用ndisasm和objdump工具反汇编bootsect、setup和system。这里我们反汇编一下system,看一下我们的C和汇编混合代码是如何链接的,链接后又是什么样子?不要被细节干扰,抓住几个关键点就可以了,如果相关知识忘记了的话可以参考《六星经典CSAPP-笔记(3)程序的机器级表示》:
- a~e:被调用函数的惯例,保存ebp挪动esp分配栈空间
- 11~19:保存两个函数参数msg(0x00010076指向的就是字符串的首地址)和len(0xf=15)到栈上
- 20:调用汇编代码中的函数myprint(),call指令自动将25行的地址压入栈上作为返回地址后跳转,call的操作数0x1b加上IP指向的0x25等于0x40,恰好是myprint()在汇编代码中的起始地址。因为call压栈挪动了esp,所以两个函数参数的位置也就从(%esp)和0x4(%esp)变成了0x4(%esp)和0x8(%esp)了
[root@localhost minios]# make disasm-sysnasm -f elf -o system/init/myprint.o system/init/myprint.asmld -Ttext 10000 -e main --oformat binary -s -x -M \ system/init/main.o \ system/init/myprint.o \ -o system/system > System.mapobjdump -b binary -m i386 -D system/systemsystem/system: file format binaryDisassembly of section .data:0000000000000000 <.data>: 0: 8d 4c 24 04 lea 0x4(%esp),%ecx 4: 83 e4 f0 and $0xfffffff0,%esp 7: ff 71 fc pushl 0xfffffffc(%ecx) a: 55 push %ebp b: 89 e5 mov %esp,%ebp d: 51 push %ecx e: 83 ec 14 sub $0x14,%esp 11: c7 44 24 04 0f 00 00 movl $0xf,0x4(%esp) 18: 00 19: c7 04 24 76 00 01 00 movl $0x10076,(%esp) 20: e8 1b 00 00 00 call 0x40 25: b8 00 00 00 00 mov $0x0,%eax ... 40: 66 b8 20 00 mov $0x20,%ax 44: 8e e8 movl %eax,%gs 46: b4 0c mov $0xc,%ah 48: bb 00 00 00 00 mov $0x0,%ebx 4d: 8b 4c 24 08 mov 0x8(%esp),%ecx 51: 8b 54 24 04 mov 0x4(%esp),%edx 55: 89 df mov %ebx,%edi 57: 81 c7 40 06 00 00 add $0x640,%edi 5d: 69 ff 02 00 00 00 imul $0x2,%edi,%edi 63: 8a 02 mov (%edx),%al 65: 65 66 89 07 mov %ax,%gs:(%edi) 69: 43 inc %ebx 6a: 49 dec %ecx 6b: 42 inc %edx 6c: 81 f9 00 00 00 00 cmp $0x0,%ecx 72: 75 e1 jne 0x55 74: eb fe jmp 0x74 76: 48 77: 65 78: 6c 79: 6c 7a: 6f 7b: 2c 20 7d: 4d 7e: 69 6e 69 4f 53 21 0a ...
4.3.3 源码解释
关于内核雏形部分的代码看起来不那么直接,直接写个C程序的main方法不就可以了吗?但这都是有原因的,解释:
- 为何要输出字符串?因为我们想直观地看到内核确实可以正常工作,体现出我们的劳动成果
- 为何要混合编程?既然要输出点什么,而这又是我们自己的操作系统内核,像printf这种系统调用我们还没有实现,所以只能靠底层的汇编代码去输出字符串。这里为了简化,我们在汇编中没有遵守正常的规则,例如保存ebp、挪动esp分配栈等
- **为何不直接在C中嵌入NASM代码?**C语言支持直接嵌入汇编代码,但不支持NASM语法的汇编代码,如果我们不想再学习另一种汇编语法的话,就只能单独拆分出一个asm文件
- 为何不直接用int 0x10中断?因为BIOS中断在保护模式下是不能用的,而我们进入保护模式后只是设置了栈的段寄存器ss,还有很多工作没做,这里为了避免出错所以用直接写显存在内存映射地址空间的方式
- 如何写显存映射空间?显存在内存中映射空间的起始地址是0B8000h,每行80个字符共35行。每个字符占用2个字节,第一个字节是字符的ASCII码,第二个字节是属性。例如第12行的最后一个字符在内存中的位置就是(80 * 11 + 79) * 2。为了简化我们直接将字符串输出到第21行,而没有读当前光标位置
4.3.4 测试
下面就测试一下我们的代码是否正确,说起来这还是我们写的第一个比较复杂的汇编代码,学习了如何在汇编语言中实现高级语言中的循环。一直执行到循环后的jmp $
一行,通过Bochs就能看到输出。如果环境没有图形界面而安装Bochs时没有安装GUI也没有关系,打印一下0xB8B40内存位置就能跳过Bochs前面的输出,看到我们刚刚输出的内容了。每个字符后面跟着的\7和\x0C就是该字符的属性了。
(0) [0x0000000000010072] 0010:00000072 (unk. ctxt): jnz .-31 (0x00010055) ; 75e1<bochs:161> Next at t=13185655(0) [0x0000000000010074] 0010:00000074 (unk. ctxt): jmp .-2 (0x00010074) ; ebfe<bochs:162> xp /500bc 0x0B8B40[bochs]:0x000b8b40 <bogus+ 0>: L \7 o \7 a \7 d \7 0x000b8b48 <bogus+ 8>: i \7 n \7 g \7 \7 0x000b8b50 <bogus+ 16>: s \7 y \7 s \7 t \7 0x000b8b58 <bogus+ 24>: e \7 m \7 . \7 . \7 0x000b8b60 <bogus+ 32>: . \7 \7 \7 \7 0x000b8b68 <bogus+ 40>: \7 \7 \7 \7 ...0x000b8c80 <bogus+ 320>: H \x0C e \x0C l \x0C l \x0C0x000b8c88 <bogus+ 328>: o \x0C , \x0C \x0C M \x0C0x000b8c90 <bogus+ 336>: i \x0C n \x0C i \x0C O \x0C0x000b8c98 <bogus+ 344>: S \x0C ! \x0C \x0A \x0C \7
5.总结
现在回头想想,似乎找到了当时阅读《Orange’s》一书时,进行到第五章就没有继续下去的原因:一是第三章“保护模式”过早地交代了保护模式的方方面面。尽管搭配了很多汇编代码丰富了示例,但也有些扰乱了学习进程,其实这一章是可以分成两部分,像中断、特权级、分页等一半的知识是可以在学会如何加载Kernel后再了解的;二是FAT和FreeDOS的引入又进一步干扰了进度。如果你只是把它当作未来Kernel的管理工具那还好,要是像我容易刨根问底的话可能就卡在那里了。其实像《30天自制操作系统》一书,不出几章很早就学完了Kernel的引导。在这一点上,《Orange’s》的作者比较细致,但也略显罗嗦。
5.1 引导过程小结
这里整理一下Orange’s与Linux 0.11对内存使用上的差异,以及各自比较麻烦的地方。
5.1.1 Orange’s引导过程
Orange’s的引导工作都是在两阶段引导程序boot和loader中完成的。整个引导过程还是比较清晰的,但有几个地方很麻烦:一是解析FAT文件系统格式将内核读取到内存;二是解析内核ELF文件头将内核的代码部分加载到指定位置。关于问题一之前也提到过,Orange’s作者使用FAT软盘和FreeDOS来方便调试。
- boot:从FAT格式软盘加载loader,打印一些信息后跳转
- loader:从FAT格式软盘加载内核,进入保护模式后准备GDT/IDT和PDT、栈段寄存器ss等后,跳转到内核
所以Orange’s的内存使用情况如下图所示:boot始终在7c00h,kernel和loader的位置也固定在30000h和90000h,PDT被放置在了高地址100000h上。下面就看一下Linux 0.11的引导过程是什么样?它是如何使用内存空间的?
; ┃ ┃ ; ┃ . ┃ ; ┃ . ┃ ; ┃ . ┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃■■■■■■■■■■■■■■■■■■┃ ; ┃■■■■■■Page Tables■■■■■■┃ ; ┃■■■■■(大小由LOADER决定)■■■■┃ ; 00101000h ┃■■■■■■■■■■■■■■■■■■┃ PageTblBase ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃■■■■■■■■■■■■■■■■■■┃ ; 00100000h ┃■■■■Page Directory Table■■■■┃ PageDirBase <- 1M ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃□□□□□□□□□□□□□□□□□□┃ ; F0000h ┃□□□□□□□System ROM□□□□□□┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃□□□□□□□□□□□□□□□□□□┃ ; E0000h ┃□□□□Expansion of system ROM □□┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃□□□□□□□□□□□□□□□□□□┃ ; C0000h ┃□□□Reserved for ROM expansion□□┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃□□□□□□□□□□□□□□□□□□┃ B8000h ← gs ; A0000h ┃□□□Display adapter reserved□□□┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃□□□□□□□□□□□□□□□□□□┃ ; 9FC00h ┃□□extended BIOS data area (EBDA)□┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃■■■■■■■■■■■■■■■■■■┃ ; 90000h ┃■■■■■■■LOADER.BIN■■■■■■┃ somewhere in LOADER ← esp ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃■■■■■■■■■■■■■■■■■■┃ ; 80000h ┃■■■■■■■KERNEL.BIN■■■■■■┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃■■■■■■■■■■■■■■■■■■┃ ; 30000h ┃■■■■■■■■KERNEL■■■■■■■┃ 30400h ← KERNEL 入口 (KernelEntryPointPhyAddr) ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃ ┃ ; 7E00h ┃ F R E E ┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃■■■■■■■■■■■■■■■■■■┃ ; 7C00h ┃■■■■■■BOOT SECTOR■■■■■■┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃ ┃ ; 500h ┃ F R E E ┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃□□□□□□□□□□□□□□□□□□┃ ; 400h ┃□□□□ROM BIOS parameter area □□┃ ; ┣━━━━━━━━━━━━━━━━━━┫ ; ┃◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇┃ ; 0h ┃◇◇◇◇◇◇Int Vectors◇◇◇◇◇◇┃ ; ┗━━━━━━━━━━━━━━━━━━┛ ← cs, ds, es, fs, ss ; ; ; ┏━━━┓ ┏━━━┓ ; ┃■■■┃ 我们使用 ┃□□□┃ 不能使用的内存 ; ┗━━━┛ ┗━━━┛ ; ┏━━━┓ ┏━━━┓ ; ┃ ┃ 未使用空间 ┃◇◇◇┃ 可以覆盖的内存 ; ┗━━━┛ ┗━━━┛
5.1.2 Linux 0.11引导过程
Linux引导程序的第一阶段与Orange’s基本一致,但第二阶段要复杂一些,主要区别在于:Linux将Orange’s的loader进入保护模式后的内核准备工作拆分到一个单独的程序head中。具体来说,Linux的system的头部其实一个汇编程序head.s编译成的,system中head后面的部分才是真正的kernel。head中负责初始化PDT、重新放置GDT/IDT等工作。为了节约内存,PDT、GDT等数据会覆盖掉head代码,所以这部分代码的技巧性很高。
5.1.3 我的MiniOS
从上面的分析能够看出,我们目前的引导过程类似Orange’s的经典两阶段引导,但本质(比如命名、放置位置、软盘上程序的加载方式等)上都是借鉴Linux的。所以前面说到的Orange’s的麻烦之处我们也巧妙地避开了:
- 关于Orange’s的第一点因为我们的bootsect和setup是连续放置的所以省了;
- 关于第二点可以看一下我们的Makefile,因为链接时丢掉了ELF各种符号表等信息,直接得到了纯二进制文件,并且在软盘上system也紧接着setup,所以第二点也可以省了。
接下来对引导和内核程序的完善过程中,我们还是更倾向于Linux,从setup中拆分出head,这样做的好处就是:内核程序以00000h为入口地址,最终进入内核时GDT、IDT、PDT等系统数据都位于00000h后的低地址,整体结构非常清晰。相比之下,Orange’s尽管引导过程能简单一些,但内核代码和系统数据比较零散,不够清晰。
- bootsect
1.1 移动自己:将自己从7c00h拷贝到90000h。因为在2.1中bootsect区域会被用来保存BIOS信息,而2.2中system会被拷贝到00000h,所以为了避免bootsect中的信息被system覆盖掉,最好现在就挪到90000h高地址
1.2 加载setup:加载setup到90200h
1.3 显示信息:输出”Loading system…”
1.4 加载system:根据SYSSIZE加载system到10000h
1.5 跳转:跳转到setup继续执行 - setup
2.1 读取内存大小:从BIOS读取内存等信息覆盖到90000h~901ffh,即bootsect的位置。因为bootsect中包含SYSSIZE,所以必须在bootsect中加载system,不能等到setup中再做
2.2 移动system:BIOS中断已经使用完毕,可以将system拷贝到00000h覆盖掉BIOS中断表了
2.3 进入保护模式:设置GDTR、A20、CR0进入保护模式
2.4 跳转:跳转到system继续执行 - system(head)
3.1 重新放置GDT:包括GDT/IDT,setup中的GDT是用于进入保护模式的临时GDT
3.2 初始化PDT:根据内存大小确定PDT大小
3.3 跳转:进入到main()函数,进入真正的内核
从上面的引导过程,也让我们清晰地了解了为什么Linux 0.11会在内存中频繁的挪动代码——bootsect拷贝自己到90000h,setup挪动system到00000h——这两次挪动的原因以及为什么system不能等到setup读取完BIOS后直接加载到00000h的原因都在上面给出了解答。
- 操作系统内核Hack:(三)引导程序制作
- 操作系统内核的引导
- 操作系统的引导(三)
- 操作系统内核Hack:(四)内核雏形
- 写操作系统(三)执着 初始引导程序
- 操作系统引导程序(nasm)
- 操作系统引导程序顺序
- 操作系统内核Hack:(一)实验环境搭建
- 操作系统内核Hack:(二)底层编程基础
- 操作系统实践之引导程序
- 操作系统引导程序学习笔记
- 自己动手写操作系统:2.C语言文件操作,制作系统引导程序
- 写操作系统(五)执着 初始引导程序 加载汇编内核
- 恢复Linux操作系统的GRUB引导程序
- 开发自己的操作系统引导程序
- 专注于操作系统4之引导程序
- 大家一起写操作系统(1)-引导程序
- 自己动手编写操作系统_引导程序
- iOS开发中 常用枚举和常用的一些运算符(易错总结)
- &&和& ;||和|
- page、request、session和application的区别
- Redis持久化之大数据服务暂停问题
- Android 抽屉菜单的实现
- 操作系统内核Hack:(三)引导程序制作
- 特征向量相似度和距离的计算
- 编译原理:第八节
- 葛爷带你上iOS王者——01
- 牛客网 | 数组中重复的数字
- 贝叶斯公式与搜索引擎
- 如何解决eclipse添加重载函数时参数为arg0,arg1的问题?
- 8皇后问题的一种简单求解
- Android 主题及自定义窗口