Hacking 《自己动手写操作系统》Chapter 4

——Writing x86 PC Bootloader With Free Software 2

本文内容已被整理为一本电子书,请到这里下载

强烈建议随《自己动手写操作系统》这本书读本篇文章,基础知识:x86 汇编指令,AT&T 汇编格式,FAT12 文件系统格式。

由于《自己动手写操作系统》第三章讲的是保护模式,示例代码全是 DOS 可执行代码,和直接的 Bootloader 没有太大关系,所以这次继续下来从第四章开始。

前两章中写的 Bootloader 其实严格意义上只算是一个 Boot sector(扇区),因为它只是简单地打印了一个字符。第四章的 Bootloader 实现了从软盘中读写一个文件并加载运行的功能。

总共有以下几个文件:boot.S loader.S solrex_x86_boot.ld solrex_x86_dos.ld Makefile

下面解释每个文件的作用:
boot.S:
它生成的 .bin 文件就是软盘镜像的第一个扇区。由上一篇文章提供的 boot.S 只有启动功能,生成的软盘镜像文件只能作为启动器而不能作为可读写软盘来使用,因为在那个镜像里面没有将软盘格式化。其实格式化说白了就是在软盘的第一个扇区添加一些头信息,这样系统就知道如何处理这张软盘,在 boot.S 中的 Floppy header 就是做这个工作的。而在软盘的前 3 个 byte 是一句跳转指令,这样在启动时候系统会跳过 floppy header,也解决了如何启动的问题。这样这张软盘既可以作为启动盘,又可以作为文件盘使用了。因为启动扇区只有 512 个字节,不可能依赖它作为启动功能,这样我们可以将另一个程序 LOADER.BIN 放入软盘,启动后,从软盘读取它进入内存并执行。

所以 boot.S 代码的主要内容是,首先将软盘主目录复制到内存中,并在其中搜索 LOADER.BIN,搜索到以后,将 LOADER.BIN 加载入内存并执行。

loader.S:
就是一个普通的打印程序,在屏幕第一行中间打印一个 'L'

solrex_x86_boot.ld:
boot.S 的连接脚本,将 boot.S 代码段连接到 0x7c00 的位置(x86 PC 操作系统启动的位置)。

solrex_x86_dos.ld:
loader.S 的连接脚本,将 loader.S 代码段连接到 0x1000 的位置,和普通 DOS 程序一样。

Makefile:
不说了,大家都知道。

这个 hacking 系列我准备继续写下去,示例代码可以从我的个人主页 http://solrex.org 打包下载,包括《自己动手写操作系统》的部分源代码,以 .asm 为后缀。对 Intel(MASM,NASM,TASM) 和 AT&T(GAS)汇编语言语法在 x86 平台的实现如何转换感兴趣的朋友,可以对比阅读 .asm 和 .S 文件。

[solrex@NJU-CAS Solrex]$ more boot.S loader.S solrex_x86_boot.ld solrex_x86_dos.ld Makefile
::::::::::::::
boot.S
::::::::::::::
.code16
.set BaseOfStack, 0x7c00 /* Stack base address, inner */
.set BaseOfLoader, 0x9000 /* Section loading address of LOADER.BIN */
.set OffsetOfLoader, 0x0100 /* Loading offset of LOADER.BIN */
.set RootDirSectors, 14 /* Root directory sector count */
.set SecNoOfRootDir, 19 /* 1st sector of root directory */
.set SecNoOfFAT1, 1 /* 1st sector of FAT1 */
.set DeltaSecNo, 17 /* BPB_(RsvdSecCnt+NumFATs*FATSz) -2 */
/* Start sector of file space =*/
.text
/* Floppy header of FAT12 */
jmp LABEL_START /* Start to boot. */
nop /* nop required */
BS_OEMName: .ascii "WB. YANG" /* OEM String, 8 bytes required */
BPB_BytsPerSec: .2byte 512 /* Bytes per sector */
BPB_SecPerCluse: .byte 1 /* Sector per cluse */
BPB_ResvdSecCnt: .2byte 1 /* Reserved sector count */
BPB_NumFATs: .byte 2 /* Number of FATs */
BPB_RootEntCnt: .2byte 224 /* Root entries count */
BPB_TotSec16: .2byte 2880 /* Total sector number */
BPB_Media: .byte 0xf0 /* Media descriptor */
BPB_FATSz16: .2byte 9 /* FAT size(sectors) */
BPB_SecPerTrk: .2byte 18 /* Sector per track */
BPB_NumHeads: .2byte 2 /* Number of magnetic heads */
BPB_HiddSec: .4byte 0 /* Number of hidden sectors */
BPB_TotSec32: .4byte 0 /* If TotSec16 equal 0, this works */
BS_DrvNum: .byte 0 /* Driver number of interrupt 13 */
BS_Reserved1: .byte 0 /* Reserved */
BS_BootSig: .byte 0x29 /* Boot signal */
BS_VolID: .4byte 0 /* Volume ID */
BS_VolLab: .ascii "Solrex 0.01" /* Volume label, 11 bytes required */
BS_FileSysType: .ascii "FAT12 " /* File system type, 8 bytes required */

/* Initial registers. */
LABEL_START:
mov %cs,%ax
mov %ax,%ds
mov %ax,%es
mov %ax,%ss
mov $BaseOfStack, %sp

/* Clear screen */
mov $0x0600,%ax /* %ah=6, %al=0 */
mov $0x0700,%bx /* Black white */
mov $0,%cx /* Top left: (0,0) */
mov $0x184f,%dx /* Bottom right: (80,50) */
int $0x10 /* BIOS int 10h, ah=6: Initialize screen */

/* Display "Booting**" */
mov $0,%dh
call DispStr /* Display string(index 0)*/

/* Reset floppy */
xor %ah,%ah
xor %dl,%dl /* %dl=0: floppy driver 0 */
int $0x13 /* BIOS int 13h, ah=0: Reset driver 0 */

/* Find LOADER.BIN in root directory of driver 0 */
movw $SecNoOfRootDir, (wSectorNo)

/* Read root dir sector to memory */
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
cmpw $0,(wRootDirSizeForLoop) /* If searching in root dir */
jz LABEL_NO_LOADERBIN /* can find LOADER.BIN ? */
decw (wRootDirSizeForLoop)
mov $BaseOfLoader,%ax
mov %ax,%es /* %es <- BaseOfLoader*/
mov $OffsetOfLoader,%bx /* %bx <- OffsetOfLoader */
mov (wSectorNo),%ax /* %ax <- sector number in root */
mov $1,%cl
call ReadSector
mov $LoaderFileName,%si /* %ds:%si -> LOADER BIN */
mov $OffsetOfLoader,%di /* BaseOfLoader<<4+100*/
cld
mov $0x10,%dx

/* Search for "LOADER BIN", FAT12 save file name in 12 bytes, 8 bytes for
file name, 3 bytes for suffix, last 1 bytes for '20'. If file name is
less than 8 bytes, filled with '20'. So "LOADER.BIN" is saved as:
"LOADER BIN"(4f4c 4441 5245 2020 4942 204e).
*/
LABEL_SEARCH_FOR_LOADERBIN:
cmp $0,%dx /* Read control */
jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR
dec %dx
mov $11,%cx

LABEL_CMP_FILENAME:
cmp $0,%cx
jz LABEL_FILENAME_FOUND /* If 11 chars are all identical? */
dec %cx
lodsb /* %ds:(%si) -> %al*/
cmp %es:(%di),%al
jz LABEL_GO_ON
jmp LABEL_DIFFERENT /* Different */

LABEL_GO_ON:
inc %di
jmp LABEL_CMP_FILENAME /* Go on loop */

LABEL_DIFFERENT:
and $0xffe0,%di /* Go to head of this entry */
add $0x20,%di
mov $LoaderFileName,%si /* Next entry */
jmp LABEL_SEARCH_FOR_LOADERBIN

LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
addw $1,(wSectorNo)
jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN

/* Not found LOADER.BIN in root dir. */
LABEL_NO_LOADERBIN:
mov $2,%dh
call DispStr /* Display string(index 2) */
jmp . /* Infinite loop */

/* Found. */
LABEL_FILENAME_FOUND:
mov $RootDirSectors,%ax
and $0xffe0,%di /* Start of current entry, 32 bytes per entry */
add $0x1a,%di /* First sector of this file */
mov %es:(%di),%cx
push %cx /* Save index of this sector in FAT */
add %ax,%cx
add $DeltaSecNo,%cx /* LOADER.BIN's start sector saved in %cl */
mov $BaseOfLoader,%ax
mov %ax,%es /* %es <- BaseOfLoader */
mov $OffsetOfLoader,%bx /* %bx <- OffsetOfLoader */
mov %cx,%ax /* %ax <- Sector number */

/* Load LOADER.BIN's sector's to memory. */
LABEL_GOON_LOADING_FILE:
push %ax
push %bx
mov $0x0e,%ah
mov $'.',%al /* Char to print */
mov $0x0f,%bl /* Front color: white */
int $0x10 /* BIOS int 10h, ah=0xe: Print char */
pop %bx
pop %ax

mov $1,%cl
call ReadSector
pop %ax /* Got index of this sector in FAT */
call GetFATEntry
cmp $0x0fff,%ax
jz LABEL_FILE_LOADED
push %ax /* Save index of this sector in FAT */
mov $RootDirSectors,%dx
add %dx,%ax
add $DeltaSecNo,%ax
add (BPB_BytsPerSec),%bx
jmp LABEL_GOON_LOADING_FILE

LABEL_FILE_LOADED:
mov $1,%dh
call DispStr /* Display string(index 1) */

/*******************************************************************
Jump to LOADER.BIN's start address in memory.
*/
jmp $BaseOfLoader,$OffsetOfLoader
/*******************************************************************/

/* ==================================================================
Variable table
*/
wRootDirSizeForLoop: .2byte RootDirSectors
wSectorNo: .2byte 0 /* Sector number to read */
bOdd: .byte 0 /* odd or even? */

/* ==================================================================
String table
*/
LoaderFileName: .asciz "LOADER BIN" /* File name */
.set MessageLength,9
BootMessage: .ascii "Booting**" /* index 0 */
Message1: .ascii "Loaded in" /* index 1 */
Message2: .ascii "No LOADER" /* index 2 */

/* ==================================================================
Routine: DispStr
Action: Display a string, string index stored in %dh
*/
DispStr:
mov $MessageLength, %ax
mul %dh
add $BootMessage,%ax
mov %ax,%bp /* String address */
mov %ds,%ax
mov %ax,%es
mov $MessageLength,%cx /* String length */
mov $0x1301,%ax /* ah = 0x13, al = 0x01(W) */
mov $0x07,%bx /* PageNum 0(bh = 0), bw(bl= 0x07)*/
mov $0,%dl /* Start row and column */
int $0x10 /* BIOS INT 10h, display string */
ret

/* ==================================================================
Routine: ReadSector
Action: Read %cl Sectors From %ax sector(floppy) to %es:%bx(memory)
Assume sector number is 'x', then:
x/(BPB_SecPerTrk) = y,
x%(BPB_SecPerTrk) = z.
The remainder 'z' PLUS 1 is the start sector number;
The quotient 'y' devide by BPB_NumHeads(RIGHT SHIFT 1 bit)is cylinder
number;
AND 'y' by 1 can got magnetic header.
*/
ReadSector:
push %ebp
mov %esp,%ebp
sub $2,%esp /* Reserve space for saving %cl */
mov %cl,-2(%ebp)
push %bx /* Save bx */
mov (BPB_SecPerTrk), %bl /* %bl: the devider */
div %bl /* 'y' in %al, 'z' in %ah */
inc %ah /* z++, got start sector */
mov %ah,%cl /* %cl <- start sector number */
mov %al,%dh /* %dh <- 'y' */
shr $1,%al /* 'y'/BPB_NumHeads */
mov %al,%ch /* %ch <- Cylinder number(y>>1) */
and $1,%dh /* %dh <- Magnetic header(y&1) */
pop %bx /* Restore %bx */
/* Now, we got cylinder number in %ch, start sector number in %cl, magnetic
header in %dh. */
mov (BS_DrvNum), %dl
GoOnReading:
mov $2,%ah
mov -2(%ebp),%al /* Read %al sectors */
int $0x13
jc GoOnReading /* If CF set 1, mean read error, reread. */
add $2,%esp
pop %ebp
ret

/* ==================================================================
Routine: GetFATEntry
Action: Find %ax sector's index in FAT, save result in %ax
*/
GetFATEntry:
push %es
push %bx
push %ax
mov $BaseOfLoader,%ax
sub $0x0100,%ax
mov %ax,%es /* Left 4K bytes for FAT */
pop %ax
movb $0,(bOdd)
mov $3,%bx
mul %bx /* %dx:%ax = %ax*3 */
mov $2,%bx
div %bx /* %dx:%ax/2 */
cmp $0,%bx /* remainder %dx = 0 ? */
jz LABEL_EVEN
movb $1,(bOdd)

LABEL_EVEN:
xor %dx,%dx /* Now %ax is the offset of FATEntry in FAT */
mov (BPB_BytsPerSec),%bx
div %bx /* %dx:%ax/BPB_BytsPerSec */
push %dx
mov $0,%bx
add $SecNoOfFAT1,%ax /* %ax <- FATEntry's sector */
mov $2,%cl /* Read 2 sectors in 1 time, because FATEntry */
call ReadSector /* may be in 2 sectors. */
pop %dx
add %dx,%bx
mov %es:(%bx),%ax
cmpb $1,(bOdd)
jnz LABEL_EVEN_2
shr $4,%ax

LABEL_EVEN_2:
and $0x0fff,%ax

LABEL_GET_FAT_ENTRY_OK:
pop %bx
pop %es
ret

.org 510 /* Skip to address 0x510. */
.2byte 0xaa55 /* Write boot flag to 1st sector(512 bytes) end */
::::::::::::::
loader.S
::::::::::::::
.code16
.text
mov $0xb800,%ax
movw %ax,%gs
mov $0xf,%ah
mov $'L',%al
mov %ax,%gs:((80*0+39)*2)
jmp .
::::::::::::::
solrex_x86_boot.ld
::::::::::::::
SECTIONS
{
. = 0x7c00;
.text :
{
_ftext = .;
} = 0
}
::::::::::::::
solrex_x86_dos.ld
::::::::::::::
SECTIONS
{
. = 0x0100;
.text :
{
_ftext = .;
} = 0
}
::::::::::::::
Makefile
::::::::::::::
CC=gcc
LD=ld
OBJCOPY=objcopy

CFLAGS=-c
TRIM_FLAGS=-R .pdr -R .comment -R.note -S -O binary

LDFILE_BOOT=solrex_x86_boot.ld
LDFILE_DOS=solrex_x86_dos.ld
LDFLAGS_BOOT=-e c -T$(LDFILE_BOOT)
LDFLAGS_DOS=-e c -T$(LDFILE_DOS)

all: boot.img LOADER.BIN

boot.bin: boot.S
$(CC) $(CFLAGS) boot.S
$(LD) boot.o -o boot.elf $(LDFLAGS_BOOT)
$(OBJCOPY) $(TRIM_FLAGS) boot.elf $@

LOADER.BIN: loader.S
$(CC) $(CFLAGS) loader.S
$(LD) loader.o -o loader.elf $(LDFLAGS_DOS)
$(OBJCOPY) $(TRIM_FLAGS) loader.elf $@

boot.img: boot.bin
dd if=/dev/zero of=emptydisk.img bs=512 count=2880
dd if=boot.bin of=boot.img bs=512 count=1
dd if=emptydisk.img of=boot.img skip=1 seek=1 bs=512 count=2879
rm emptydisk.img

# You must have the authority to do mount, or you must use "su root" or
# "sudo" command to do "make copy"
copy: boot.img LOADER.BIN
mkdir -p /tmp/floppy;
mount -o loop boot.img /tmp/floppy/ -o fat=12;
cp LOADER.BIN /tmp/floppy/;
umount /tmp/floppy/;
rm -rf /tmp/floppy/;

clean:
@rm -f *.o *.elf *.bin *.BIN

distclean:
@rm -f *.img

《Hacking 《自己动手写操作系统》Chapter 4》上有6条评论

  1. 那本书我大二时看过了,很久了。 8)
    听你这么一说,倒是想起我以前的一个主意:写一个intel汇编和at&t汇编互相转换的脚本。这样可以利用充分某些汇编代码,比如你上面的那些。怎么样?有兴趣的话可以考虑一起开始做。

  2. [Comment ID #152136 Will Be Quoted Here]
    其实网上已经有这个项目了,叫做 intel2gas,但是转换效果并不理想,而且好像已经陷入停滞。
    我也有这个想法,但是我最近时间不多,而且暂时还没有自己的电脑,所以还没开始写。我的想法是给 intel2gas 添加 patch,让它更完善。
    但如果你能使用某些脚本程序,比如 python 之类的完成,那也是相当不错的。我只是觉得如果用 bash 来实现,字符串处理太麻烦,不如 tcl, python 等脚本语言。

回复 Dean 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注