ELF文件格式
ELF = Executable and Linkable Format,可执行连接格式,是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的。扩展名为elf。
其主要有三种主要类型: 适于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。 适于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。 共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
文件格式
为了方便和高效,ELF文件内容有两个平行的视角:一个是程序连接角度,另一个是程序运行角度,如图所示。
ELF header在文件开始处描述了整个文件的组织,Section提供了目标文件的各项信息(如指令、数据、符号表、重定位信息等),Program header table指出怎样创建进程映像,含有每个program header的入口,section header table包含每一个section的入口,给出名字、大小等信息。其中Segment与section的关系后面会讲到。
要理解这个图,我们还要认识下ELF文件相关的几个重要的结构体:
1,ELF文件头 像bmp、exe等文件一样,ELF的文件头包含整个文件的控制结构。它的定义如下,可在/usr/include/elf.h中可以找到文件头结构定义:
/* The ELF file header. This appears at the start of every ELF file. */
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
其中e_ident的16个字节标识是个ELF文件(7F+’E’+’L’+’F’)。 e_type表示文件类型,2表示可执行文件。 e_machine说明机器类别,3表示386机器,8表示MIPS机器。 e_entry给出进程开始的虚地址,即系统将控制转移的位置。 e_phoff指出program header table的文件偏移。 e_phentsize表示一个program header表中的入口的长度(字节数表示)。 e_phnum给出program header表中的入口数目。类似的。 e_shoff,e_shentsize,e_shnum 分别表示section header表的文件偏移,表中每个入口的的字节数和入口数目。 e_flags给出与处理器相关的标志。 e_ehsize给出ELF文件头的长度(字节数表示)。 e_shstrndx表示section名表的位置,指出在section header表中的索引。
由于ELF文件力求支持从8位到32位不同架构的处理器,所以才定义了表中这些数据类型,从而让文件格式与机器无关!
名称 | 大小 | 对齐 | 用途
- |-|-|-
Elf32_Addr | 4 | 4 | 无符号程序地址 |
Elf32_Half | 2 | 2 | 无符号中等大小整数 |
Elf32_Off | 4 | 4 | 无符号文件偏移 |
Elf32_Sword | 4 | 4 | 有符号大整数 |
Elf32_word | 4 | 4 | 无符号大整数 |
unsigned char | 1 | 1 | 无符号小整数 |
xxd命令看一下foobar
表示显示0-80个地址,16进制!
e_type 在foobar 是2表示可执行文件。 e_machine的值是3表示386机器。 e_entry给出进程开始的虚地址,即系统将控制转移的位置,在foobar中地址是0x80480A0。 e_phoff指出program header table的文件偏移,这里的值是0x34。 e_phentsize表示一个program header table表中的入口的长度(字节数表示),这里的值是0x20。 e_phnum给出program header表中的入口数目。这里的值是3个,类似的。 e_shoff,e_shentsize,e_shnum 分别表示section header表的文件偏移,这里的值是0x107C,表中每个入口的的字节数,这里的值是0x20和入口数目,这里的值是6个。 e_flags给出与处理器相关的标志,针对IA32而言,此项目是0。 e_ehsize给出ELF文件头的长度(字节数表示)。 e_shstrndx表示section名表的位置,指出在section header表中的索引。
2,Program header 目标文件或者共享文件的program header table描述了系统执行一个程序所需要的段或者其它信息。目标文件的一个段(segment)包含一个或者多个section。Program header只对可执行文件和共享目标文件有意义,对于程序的链接没有任何意义。结构定义如下,可在/usr/include/elf.h中可以找到文件头结构定义:
/* Program segment header. */
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
实际上Program header描述的是系统准备程序运行所需的一个段(Segment)或其他信息。 举例,看看foobar 的情况。程序头表共有三项(e_phnum=3),因为前面得知是0x34后面就是program,且大小是0x20. 所以偏移量是0x34~0x53 , 0x54~0x73 , 0x74~0x93.
xxd命令看一下地址
其中p_type描述段的类型; p_offset给出该段相对于文件开关的偏移量; p_vaddr给出该段所在的虚拟地址; p_paddr给出该段的物理地址; p_filesz给出该段的大小,在字节为单元,可能为0; p_memsz给出该段在内存中所占的大小,可能为0; p_filesze与p_memsz的值可能会不相等。
Program header描述的是一个段在文件中的位置、大小以及它被放进内存后所在的位置和大小。如果我们想把一个文件加载进内存的话,需要的正是这个信息!可执行文件中,一个program header描述的内容称为一个段(segment)。Segment包含一个或者多个section!
在foobar 共有三个Program header 取值如表所示
名称 | Program header 0 | Program header 1 | Program header 2
- |-|-|-
P_type | 0x01 | 0x01 | 0x6474E551 |
p_offset | 0x00 | 0x1000 | 0 |
p_vaddr | 0x8048000 | 0x804A000 | 0 |
p_paddr | 0x8048000 | 0x804A000 | 0 |
p_filesz | 0x194 | 0x14 | 0 |
p_memsz | 0x194 | 0x14 | 0 |
p_flags | 0x05 | 0x06 | 0x07 |
p_align | 0x1000 | 0x1000 | 0x10 |
根据信息就知道foobar加载进内存之后的情形!
Section Header
目标文件的section header table可以定位所有的section,它是一个Elf32_Shdr结构的数组,Section头表的索引是这个数组的下标。有些索引号是保留的,目标文件不能使用这些特殊的索引。 Section包含目标文件除了ELF文件头、程序头表、section头表的所有信息,而且目标文件section满足几个条件: 目标文件中的每个section都只有一个section头项描述,可以存在不指示任何section的section头项。 每个section在文件中占据一块连续的空间。 Section之间不可重叠。 目标文件可以有非活动空间,各种headers和sections没有覆盖目标文件的每一个字节,这些非活动空间是没有定义的。 Section header结构定义如下,可在/usr/include/elf.h中可以找到文件头结构定义:
/* Section header. */
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
其中sh_name指出section的名字,它的值是后面将会讲到的section header string table中的偏移,指出一个以null结尾的字符串。 sh_type是类别。 sh_flags指示该section在进程执行时的特性。 sh_addr指出若此section在进程的内存映像中出现,则给出开始的虚地址。 sh_offset给出此section在文件中的偏移。其它字段的意义不太常用,在此不细述。
文件的section含有程序和控制信息,系统使用一些特定的section,并有其固定的类型和属性(由sh_type和sh_info指出)。下面介绍几个常用到的section:“.bss”段含有占据程序内存映像的未初始化数据,当程序开始运行时系统对这段数据初始为零,但这个section并不占文件空间。“.data.”和“.data1”段包含占据内存映像的初始化数据。“.rodata”和“.rodata1”段含程序映像中的只读数据。“.shstrtab”段含有每个section的名字,由section入口结构中的sh_name索引值来获取。“.strtab”段含有表示符号表(symbol table)名字的字符串。“.symtab”段含有文件的符号表,在后文专门介绍。“.text”段包含程序的可执行指令。
Symbol Table
目标文件的符号表包含定位或重定位程序符号定义和引用时所需要的信息。符号表入口结构定义如下,可在/usr/include/elf.h中可以找到文件头结构定义:
/* Symbol table entry. */
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
其中st_name包含指向符号表字符串表(strtab)中的索引,从而可以获得符号名。 st_value指出符号的值,可能是一个绝对值、地址等。 st_size指出符号相关的内存大小,比如一个数据结构包含的字节数等。 st_info规定了符号的类型和绑定属性,指出这个符号是一个数据名、函数名、section名还是源文件名;并且指出该符号的绑定属性是local、global还是weak。
书上并没有继续分析sector 和symbol 的信息,不过网上有一大堆,可以参考学习!
从Loader到内核
之前学习的Loader需要做的工作就是
- 加载内核到内存
- 跳入保护模式
用Loader加载ELF 文件
定义了一个常量头文件,用于boot.asm 和 loader.asm之间共享!把FAT12文件有关的内容写进一个单独的文件(文件名为fat12hdr.inc) 在两个文件的开头相应位置分别包含进去!
1 ; FAT12 磁盘的头
2 ; ----------------------------------------------------------------------
3 BS_OEMName DB 'ForrestY' ; OEM String, 必须 8 个字节
4
5 BPB_BytsPerSec DW 512 ; 每扇区字节数
6 BPB_SecPerClus DB 1 ; 每簇多少扇区
7 BPB_RsvdSecCnt DW 1 ; Boot 记录占用多少扇区
8 BPB_NumFATs DB 2 ; 共有多少 FAT 表
9 BPB_RootEntCnt DW 224 ; 根目录文件数最大值
10 BPB_TotSec16 DW 2880 ; 逻辑扇区总数
11 BPB_Media DB 0xF0 ; 媒体描述符
12 BPB_FATSz16 DW 9 ; 每FAT扇区数
13 BPB_SecPerTrk DW 18 ; 每磁道扇区数
14 BPB_NumHeads DW 2 ; 磁头数(面数)
15 BPB_HiddSec DD 0 ; 隐藏扇区数
16 BPB_TotSec32 DD 0 ; 如果 wTotalSectorCount 是 0 由这个值记录扇区数
17
18 BS_DrvNum DB 0 ; 中断 13 的驱动器号
19 BS_Reserved1 DB 0 ; 未使用
20 BS_BootSig DB 29h ; 扩展引导标记 (29h)
21 BS_VolID DD 0 ; 卷序列号
22 BS_VolLab DB 'OrangeS0.02'; 卷标, 必须 11 个字节
23 BS_FileSysType DB 'FAT12 ' ; 文件系统类型, 必须 8个字节
24 ;------------------------------------------------------------------------
25
26
27 ; -------------------------------------------------------------------------
28 ; 基于 FAT12 头的一些常量定义,如果头信息改变,下面的常量可能也要做相应改变
29 ; -------------------------------------------------------------------------
30 ; BPB_FATSz16
31 FATSz equ 9
32
33 ; 根目录占用空间:
34 ; RootDirSectors = ((BPB_RootEntCnt*32)+(BPB_BytsPerSec–1))/BPB_BytsPerSec
35 ; 但如果按照此公式代码过长,故定义此宏
36 RootDirSectors equ 14
37
38 ; Root Directory 的第一个扇区号 = BPB_RsvdSecCnt + (BPB_NumFATs * FATSz)
39 SectorNoOfRootDirectory equ 19
40
41 ; FAT1 的第一个扇区号 = BPB_RsvdSecCnt
42 SectorNoOfFAT1 equ 1
43
44 ; DeltaSectorNo = BPB_RsvdSecCnt + (BPB_NumFATs * FATSz) - 2
45 ; 文件的开始Sector号 = DirEntry中的开始Sector号 + 根目录占用Sector数目
46 ; + DeltaSectorNo
47 DeltaSectorNo equ 17
48
在boot.asm 开头代码就是
; 下面是 FAT12 磁盘的头, 之所以包含它是因为下面用到了磁盘的一些信息
%include "fat12hdr.inc"
修改loader.asm ,把他内核放进内存
1 org 0100h
2
3 BaseOfStack equ 0100h
4
5 BaseOfKernelFile equ 08000h ; KERNEL.BIN 被加载到的位置 ---- 段地址
6 OffsetOfKernelFile equ 0h ; KERNEL.BIN 被加载到的位置 ---- 偏移地址
7
8
9 jmp LABEL_START ; Start
10
11 ; 下面是 FAT12 磁盘的头, 之所以包含它是因为下面用到了磁盘的一些信息
12 %include "fat12hdr.inc"
13
14
15 LABEL_START: ; <--- 从这里开始 *************
16 mov ax, cs
17 mov ds, ax
18 mov es, ax
19 mov ss, ax
20 mov sp, BaseOfStack
21
22 mov dh, 0 ; "Loading "
23 call DispStr ; 显示字符串
24
25 ; 下面在 A 盘的根目录寻找 KERNEL.BIN
26 mov word [wSectorNo], SectorNoOfRootDirectory
27 xor ah, ah ; `.
28 xor dl, dl ; | 软驱复位
29 int 13h ; /
30 LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
31 cmp word [wRootDirSizeForLoop], 0 ; `.
32 jz LABEL_NO_KERNELBIN ; | 判断根目录区是不是已经读完,
33 dec word [wRootDirSizeForLoop] ; / 读完表示没有找到 KERNEL.BIN
34 mov ax, BaseOfKernelFile
35 mov es, ax ; es <- BaseOfKernelFile
36 mov bx, OffsetOfKernelFile ; bx <- OffsetOfKernelFile
37 mov ax, [wSectorNo] ; ax <- Root Directory 中的某 Sector 号
38 mov cl, 1
39 call ReadSector
40
41 mov si, KernelFileName ; ds:si -> "KERNEL BIN"
42 mov di, OffsetOfKernelFile
43 cld
44 mov dx, 10h
45 LABEL_SEARCH_FOR_KERNELBIN:
46 cmp dx, 0 ; `.
47 jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR; | 循环次数控制, 如果已经读完
48 dec dx ; / 了一个 Sector, 就跳到下一个
49 mov cx, 11
50 LABEL_CMP_FILENAME:
51 cmp cx, 0 ; `.
52 jz LABEL_FILENAME_FOUND ; | 循环次数控制, 如果比较了 11 个字符都
53 dec cx ; / 相等, 表示找到
54 lodsb ; ds:si -> al
55 cmp al, byte [es:di] ; if al == es:di
56 jz LABEL_GO_ON
57 jmp LABEL_DIFFERENT
58 LABEL_GO_ON:
59 inc di
60 jmp LABEL_CMP_FILENAME ; 继续循环
61
62 LABEL_DIFFERENT:
63 and di, 0FFE0h ; else`. 让 di 是 20h 的倍数
64 add di, 20h ; |
65 mov si, KernelFileName ; | di += 20h 下一个目录条目
66 jmp LABEL_SEARCH_FOR_KERNELBIN; /
67
68 LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
69 add word [wSectorNo], 1
70 jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN
71
72 LABEL_NO_KERNELBIN:
73 mov dh, 2 ; "No KERNEL."
74 call DispStr ; 显示字符串
75 %ifdef _LOADER_DEBUG_
76 mov ax, 4c00h ; `.
77 int 21h ; / 没有找到 KERNEL.BIN, 回到 DOS
78 %else
79 jmp $ ; 没有找到 KERNEL.BIN, 死循环在这里
80 %endif
81
82 LABEL_FILENAME_FOUND: ; 找到 KERNEL.BIN 后便来到这里继续
83 mov ax, RootDirSectors
84 and di, 0FFF0h ; di -> 当前条目的开始
85
86 push eax
87 mov eax, [es : di + 01Ch] ; `.
88 mov dword [dwKernelSize], eax ; / 保存 KERNEL.BIN 文件大小
89 pop eax
90
91 add di, 01Ah ; di -> 首 Sector
92 mov cx, word [es:di]
93 push cx ; 保存此 Sector 在 FAT 中的序号
94 add cx, ax
95 add cx, DeltaSectorNo ; cl <- KERNEL.BIN 的起始扇区号(0-based)
96 mov ax, BaseOfKernelFile
97 mov es, ax ; es <- BaseOfKernelFile
98 mov bx, OffsetOfKernelFile ; bx <- OffsetOfKernelFile
99 mov ax, cx ; ax <- Sector 号
100
101 LABEL_GOON_LOADING_FILE:
102 push ax ; `.
103 push bx ; |
104 mov ah, 0Eh ; | 每读一个扇区就在 "Loading " 后面
105 mov al, '.' ; | 打一个点, 形成这样的效果:
106 mov bl, 0Fh ; | Loading ......
107 int 10h ; |
108 pop bx ; |
109 pop ax ; /
110
111 mov cl, 1
112 call ReadSector
113 pop ax ; 取出此 Sector 在 FAT 中的序号
114 call GetFATEntry
115 cmp ax, 0FFFh
116 jz LABEL_FILE_LOADED
117 push ax ; 保存 Sector 在 FAT 中的序号
118 mov dx, RootDirSectors
119 add ax, dx
120 add ax, DeltaSectorNo
121 add bx, [BPB_BytsPerSec]
122 jmp LABEL_GOON_LOADING_FILE
123 LABEL_FILE_LOADED:
124
125 call KillMotor ; 关闭软驱马达
126
127 mov dh, 1 ; "Ready."
128 call DispStr ; 显示字符串
129
130 jmp $
131
132
133 ;============================================================================
134 ;变量
135 ;----------------------------------------------------------------------------
136 wRootDirSizeForLoop dw RootDirSectors ; Root Directory 占用的扇区数
137 wSectorNo dw 0 ; 要读取的扇区号
138 bOdd db 0 ; 奇数还是偶数
139 dwKernelSize dd 0 ; KERNEL.BIN 文件大小
140
141 ;============================================================================
142 ;字符串
143 ;----------------------------------------------------------------------------
144 KernelFileName db "KERNEL BIN", 0 ; KERNEL.BIN 之文件名
145 ; 为简化代码, 下面每个字符串的长度均为 MessageLength
146 MessageLength equ 9
147 LoadMessage: db "Loading "
148 Message1 db "Ready. "
149 Message2 db "No KERNEL"
150 ;============================================================================
151
152 ;----------------------------------------------------------------------------
153 ; 函数名: DispStr
154 ;----------------------------------------------------------------------------
155 ; 作用:
156 ; 显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
157 DispStr:
158 mov ax, MessageLength
159 mul dh
160 add ax, LoadMessage
161 mov bp, ax ; ┓
162 mov ax, ds ; ┣ ES:BP = 串地址
163 mov es, ax ; ┛
164 mov cx, MessageLength ; CX = 串长度
165 mov ax, 01301h ; AH = 13, AL = 01h
166 mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h)
167 mov dl, 0
168 add dh, 3 ; 从第 3 行往下显示
169 int 10h ; int 10h
170 ret
171 ;----------------------------------------------------------------------------
172 ; 函数名: ReadSector
173 ;----------------------------------------------------------------------------
174 ; 作用:
175 ; 从序号(Directory Entry 中的 Sector 号)为 ax 的的 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
176 ReadSector:
177 ; -----------------------------------------------------------------------
178 ; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号)
179 ; -----------------------------------------------------------------------
180 ; 设扇区号为 x
181 ; ┌ 柱面号 = y >> 1
182 ; x ┌ 商 y ┤
183 ; -------------- => ┤ └ 磁头号 = y & 1
184 ; 每磁道扇区数 │
185 ; └ 余 z => 起始扇区号 = z + 1
186 push bp
187 mov bp, sp
188 sub esp, 2 ; 辟出两个字节的堆栈区域保存要读的扇区数: byte [bp-2]
189
190 mov byte [bp-2], cl
191 push bx ; 保存 bx
192 mov bl, [BPB_SecPerTrk] ; bl: 除数
193 div bl ; y 在 al 中, z 在 ah 中
194 inc ah ; z ++
195 mov cl, ah ; cl <- 起始扇区号
196 mov dh, al ; dh <- y
197 shr al, 1 ; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)
198 mov ch, al ; ch <- 柱面号
199 and dh, 1 ; dh & 1 = 磁头号
200 pop bx ; 恢复 bx
201 ; 至此, "柱面号, 起始扇区, 磁头号" 全部得到 ^^^^^^^^^^^^^^^^^^^^^^^^
202 mov dl, [BS_DrvNum] ; 驱动器号 (0 表示 A 盘)
203 .GoOnReading:
204 mov ah, 2 ; 读
205 mov al, byte [bp-2] ; 读 al 个扇区
206 int 13h
207 jc .GoOnReading ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止
208
209 add esp, 2
210 pop bp
211
212 ret
213
214 ;----------------------------------------------------------------------------
215 ; 函数名: GetFATEntry
216 ;----------------------------------------------------------------------------
217 ; 作用:
218 ; 找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
219 ; 需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
220 GetFATEntry:
221 push es
222 push bx
223 push ax
224 mov ax, BaseOfKernelFile ; ┓
225 sub ax, 0100h ; ┣ 在 BaseOfKernelFile 后面留出 4K 空间用于存放 FAT
226 mov es, ax ; ┛
227 pop ax
228 mov byte [bOdd], 0
229 mov bx, 3
230 mul bx ; dx:ax = ax * 3
231 mov bx, 2
232 div bx ; dx:ax / 2 ==> ax <- 商, dx <- 余数
233 cmp dx, 0
234 jz LABEL_EVEN
235 mov byte [bOdd], 1
236 LABEL_EVEN:;偶数
237 xor dx, dx ; 现在 ax 中是 FATEntry 在 FAT 中的偏移量. 下面来计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)
238 mov bx, [BPB_BytsPerSec]
239 div bx ; dx:ax / BPB_BytsPerSec ==> ax <- 商 (FATEntry 所在的扇区相对于 FAT 来说的扇区号)
240 ; dx <- 余数 (FATEntry 在扇区内的偏移)。
241 push dx
242 mov bx, 0 ; bx <- 0 于是, es:bx = (BaseOfKernelFile - 100):00 = (BaseOfKernelFile - 100) * 10h
243 add ax, SectorNoOfFAT1 ; 此句执行之后的 ax 就是 FATEntry 所在的扇区号
244 mov cl, 2
245 call ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界发生错误, 因为一个 FATEntry 可能跨越两个扇区
246 pop dx
247 add bx, dx
248 mov ax, [es:bx]
249 cmp byte [bOdd], 1
250 jnz LABEL_EVEN_2
251 shr ax, 4
252 LABEL_EVEN_2:
253 and ax, 0FFFh
254
255 LABEL_GET_FAT_ENRY_OK:
256
257 pop bx
258 pop es
259 ret
260 ;----------------------------------------------------------------------------
261
262
263 ;----------------------------------------------------------------------------
264 ; 函数名: KillMotor
265 ;----------------------------------------------------------------------------
266 ; 作用:
267 ; 关闭软驱马达
268 KillMotor:
269 push dx
270 mov dx, 03F2h
271 mov al, 0
272 out dx, al
273 pop dx
274 ret
275 ;----------------------------------------------------------------------------
276
277
可以看出Loader.asm 比上次增加了很多代码,但是和boot.asm 对比发现,是几乎一样的! 的确如书上所说,增加了call KillMotor ; 关闭软驱马达
加载内核代码写好了,还缺个内核的东东!
kernel.asm 代码实现功能还是一个字符功能!
1 ; 编译链接方法
2 ; $ nasm -f elf kernel.asm -o kernel.o
3 ; $ ld -s kernel.o -o kernel.bin #‘-s’选项意为“strip all”
4
5 [section .text] ; 代码在此
6
7 global _start ; 导出 _start
8
9 _start: ; 跳到这里来的时候,我们假设 gs 指向显存
10 mov ah, 0Fh ; 0000: 黑底 1111: 白字
11 mov al, 'K'
12 mov [gs:((80 * 1 + 39) * 2)], ax ; 屏幕第 1 行, 第 39 列
13 jmp $
14
运行结果如下:
跳入保护模式
现在内核加载进去内存了,要跳入保护模式了
GDT 和对应的选择子,三个描述符,分别是0~4GB的可读写段和一个指向显存开始地址的段!
loader.asm
%include "load.inc"
%include "pm.inc"
; GDT
; 段基址 段界限, 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_FLAT_C: Descriptor 0, 0fffffh, DA_CR|DA_32|DA_LIMIT_4K ;0-4G
LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, DA_DRW|DA_32|DA_LIMIT_4K;0-4G
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW|DA_DPL3 ; 显存首地址
GdtLen equ $ - LABEL_GDT
GdtPtr dw GdtLen - 1 ; 段界限
dd BaseOfLoaderPhyAddr + LABEL_GDT ; 基地址
; GDT 选择子
SelectorFlatC equ LABEL_DESC_FLAT_C - LABEL_GDT
SelectorFlatRW equ LABEL_DESC_FLAT_RW - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT + SA_RPL3
BaseOfStack equ 0100h
PageDirBase equ 100000h ; 页目录开始地址: 1M
PageTblBase equ 101000h ; 页表开始地址: 1M + 4K
前面学习的时候,描述符的段基址都是运行时计算后填入的相应的位置。现在我们也不知道段地址,自然也不知道程序运行时在内存的位置。
Loader 是我们自己加载的,段地址被确定为BaseOfLoader,所以在Loader中出现的标号(变量)的物理地址可以用下面的公式来表示:
标号(变量) 的物理地址 = BaseOfLoader X 10h + 标号(变量)的偏移。
BaseOfLoader就同时在boot.asm 和loader.asm两个文件中使用,我们也把它和相应的声明放在load.inc 文件中。
load.inc 宏定义
BaseOfLoader equ 09000h ; LOADER.BIN 被加载到的位置 ---- 段地址
OffsetOfLoader equ 0100h ; LOADER.BIN 被加载到的位置 ---- 偏移地址
BaseOfLoaderPhyAddr equ BaseOfLoader*10h ; LOADER.BIN 被加载到的位置 ---- 物理地址
BaseOfKernelFile equ 08000h ; KERNEL.BIN 被加载到的位置 ---- 段地址
OffsetOfKernelFile equ 0h ; KERNEL.BIN 被加载到的位置 ---- 偏移地址
都用%include “load.inc”包含!
Loader.asm 32位代码段只打印一个字符 ‘P’。
进入保护模式的代码不附上了!
初始化其他寄存器,
mov ax, SelectorFlatRW
mov ds, ax
mov es, ax
mov fs, ax
mov ss, ax
mov esp, TopOfStack
然后打开分页机制,打开之前需要得到可用内容的情况!
; 得到内存数
mov ebx, 0 ; ebx = 后续值, 开始时需为 0
mov di, _MemChkBuf ; es:di 指向一个地址范围描述符结构(ARDS)
.MemChkLoop:
mov eax, 0E820h ; eax = 0000E820h
mov ecx, 20 ; ecx = 地址范围描述符结构的大小
mov edx, 0534D4150h ; edx = 'SMAP'
int 15h ; int 15h
jc .MemChkFail
add di, 20
inc dword [_dwMCRNumber] ; dwMCRNumber = ARDS 的个数
cmp ebx, 0
jne .MemChkLoop
jmp .MemChkOK
.MemChkFail:
mov dword [_dwMCRNumber], 0
.MemChkOK:
显示内存信息
; 显示内存信息 --------------------------------------------------------------
DispMemInfo:
push esi
push edi
push ecx
mov esi, MemChkBuf
mov ecx, [dwMCRNumber];for(int i=0;i<[MCRNumber];i++)//每次得到一个ARDS
.loop: ;{
mov edx, 5 ; for(int j=0;j<5;j++)//每次得到一个ARDS中的成员
mov edi, ARDStruct ; {//依次显示:BaseAddrLow,BaseAddrHigh,LengthLow
.1: ; LengthHigh,Type
push dword [esi] ;
call DispInt ; DispInt(MemChkBuf[j*4]); // 显示一个成员
pop eax ;
stosd ; ARDStruct[j*4] = MemChkBuf[j*4];
add esi, 4 ;
dec edx ;
cmp edx, 0 ;
jnz .1 ; }
call DispReturn ; printf("\n");
cmp dword [dwType], 1 ; if(Type == AddressRangeMemory)
jne .2 ; {
mov eax, [dwBaseAddrLow];
add eax, [dwLengthLow];
cmp eax, [dwMemSize] ; if(BaseAddrLow + LengthLow > MemSize)
jb .2 ;
mov [dwMemSize], eax ; MemSize = BaseAddrLow + LengthLow;
.2: ; }
loop .loop ;}
;
call DispReturn ;printf("\n");
push szRAMSize ;
call DispStr ;printf("RAM size:");
add esp, 4 ;
;
push dword [dwMemSize] ;
call DispInt ;DispInt(MemSize);
add esp, 4 ;
pop ecx
pop edi
pop esi
ret
; ---------------------------------------------------------------------------
后面就是启动分页函数的SetupPaging
; 启动分页机制 --------------------------------------------------------------
SetupPaging:
; 根据内存大小计算应初始化多少PDE以及多少页表
xor edx, edx
mov eax, [dwMemSize]
mov ebx, 400000h ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小
div ebx
mov ecx, eax ; 此时 ecx 为页表的个数,也即 PDE 应该的个数
test edx, edx
jz .no_remainder
inc ecx ; 如果余数不为 0 就需增加一个页表
.no_remainder:
push ecx ; 暂存页表个数
; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.
; 首先初始化页目录
mov ax, SelectorFlatRW
mov es, ax
mov edi, PageDirBase ; 此段首地址为 PageDirBase
xor eax, eax
mov eax, PageTblBase | PG_P | PG_USU | PG_RWW
.1:
stosd
add eax, 4096 ; 为了简化, 所有页表在内存中是连续的
loop .1
; 再初始化所有页表
pop eax ; 页表个数
mov ebx, 1024 ; 每个页表 1024 个 PTE
mul ebx
mov ecx, eax ; PTE个数 = 页表个数 * 1024
mov edi, PageTblBase ; 此段首地址为 PageTblBase
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.2:
stosd
add eax, 4096 ; 每一页指向 4K 的空间
loop .2
mov eax, PageDirBase
mov cr3, eax
mov eax, cr0
or eax, 80000000h
mov cr0, eax
jmp short .3
.3:
nop
ret
; 分页机制启动完毕 ----------------------------------------------------------
代码使用的字符串和变量的定义
LABEL_DATA:
; 实模式下使用这些符号
; 字符串
_szMemChkTitle: db "BaseAddrL BaseAddrH LengthLow LengthHigh Type", 0Ah, 0
_szRAMSize: db "RAM size:", 0
_szReturn: db 0Ah, 0
;; 变量
_dwMCRNumber: dd 0 ; Memory Check Result
_dwDispPos: dd (80 * 6 + 0) * 2 ; 屏幕第 6 行, 第 0 列
_dwMemSize: dd 0
_ARDStruct: ; Address Range Descriptor Structure
_dwBaseAddrLow: dd 0
_dwBaseAddrHigh: dd 0
_dwLengthLow: dd 0
_dwLengthHigh: dd 0
_dwType: dd 0
_MemChkBuf: times 256 db 0
;
;; 保护模式下使用这些符号
szMemChkTitle equ BaseOfLoaderPhyAddr + _szMemChkTitle
szRAMSize equ BaseOfLoaderPhyAddr + _szRAMSize
szReturn equ BaseOfLoaderPhyAddr + _szReturn
dwDispPos equ BaseOfLoaderPhyAddr + _dwDispPos
dwMemSize equ BaseOfLoaderPhyAddr + _dwMemSize
dwMCRNumber equ BaseOfLoaderPhyAddr + _dwMCRNumber
ARDStruct equ BaseOfLoaderPhyAddr + _ARDStruct
dwBaseAddrLow equ BaseOfLoaderPhyAddr + _dwBaseAddrLow
dwBaseAddrHigh equ BaseOfLoaderPhyAddr + _dwBaseAddrHigh
dwLengthLow equ BaseOfLoaderPhyAddr + _dwLengthLow
dwLengthHigh equ BaseOfLoaderPhyAddr + _dwLengthHigh
dwType equ BaseOfLoaderPhyAddr + _dwType
MemChkBuf equ BaseOfLoaderPhyAddr + _MemChkBuf
可以看出使用的地址都加上了Loader的基地址了!
调用显示内存信息和启动分页的函数
push szMemChkTitle
call DispStr
add esp, 4
call DispMemInfo
call SetupPaging
重新放置内核
我们要根据内核的Program header table 的信息类似c语言进行内存复制
memcpy(p_vaddr, BaseOfLoaderPhyAddr + p_offset , p_filesz);
有多少个Program header,就复制多少次!
前面的提到的elf 中 program header都描述一个段,p_offset为段在文件中的偏移,p_filesz为段在文件中的长度,p_vaddr为段在内存中的虚拟地址。
书上提到了,比如ld生成的可执行文件p_vaddr的值总是一个类似于0x8048xxx的值,至少例子是这样的一个值。可是我们启动分页机制时地址都是对等映射的,内存地址0x8048xxx已经处于128Mb内存意外的(128Mb的十六进制表示0x8000000),如果计算机的内存小于128Mb的话,这个地址显然超出内存大小! 即使内存足够大,也不能让编译器决定内核加载到什么地方!有两个解决办法,一个是通过修改页表让0x80048xxx映射到较低的地址,另外一种方法就是通过修改ld的选项让他生成的可执行代码中p_vaddr的值变小。
显然第二种办法简单易行,把编译链接的命令行改为:
nasm -f elf -o kernel.o kernel.asm
ld -s -Ttext 0x30400 -o kernel.bin kernel.o
程序的入口地址就变成0x30400了,elf header等信息会位于0x30400之前! 此时的elf header 和 program header table情况如表所示
项目 | 值 | 说明 |
- |-|-
e_ident | …. |
e_type | 2H | 可执行文件 |
e_machine | 3H | 80386 |
e_version | 1H |
e_entry | 30400H | 入口地址 |
e_phoff | 34H | Program header table在文件中的偏移量 |
e_shoff | 448H | Section header table 在文件中的偏移量 |
e_flags | 0H |
e_ehsizes | 34H | ELF header 大小 |
e_phentsize | 28H | 每一个Section header 大小28H字节 |
e_shnum | 4H | Section header table 中有四个条目 |
e_shstrndx | 3H | 包含节名称的字符串表示第三个节 |
Program header
项目 | 值 | 说明 |
- |-|-
p_type | 1H | PT_LOAD |
p_offset | 0H | 段的第一个字节在文件中的偏移 |
p_vaddr | 30000H | 段的第一个字节在内存中虚拟地址 |
p_paddr | 30000H |
p_filesz | 40DH | 段在文件中的长度 |
p_memsz | 40DH | 段在内存中的长度 |
e_flags | 5H |
e_ehsizes | 1000H |
也就说,我们应该把文件从开头开始40d字节的内容放到内存30000h处。由于程序的入口在30400h处,所以,从这里看出实际代码只有0Dh + 1 个字节。 如图是操作的结果!
星号省去的部分都是0.
从中可以看出,从400h到40dh 是仅有的代码,看代码 EB FE 就是代码的最后的”jmp $” (EB, FE 相当于jmp $死循环)!
下面需要将kernel.bin 根据ELf 文件信息转移到正确的位置!就是找出每个Program header,根据信息进行内存复制!
InitKernel :
call InitKernel
....
; InitKernel ---------------------------------------------------------------------------------
; 将 KERNEL.BIN 的内容经过整理对齐后放到新的位置
; 遍历每一个 Program Header,根据 Program Header 中的信息来确定把什么放进内存,放到什么位置,以及放多少。
; --------------------------------------------------------------------------------------------
InitKernel:
xor esi, esi
mov cx, word [BaseOfKernelFilePhyAddr+2Ch];`. ecx <- pELFHdr->e_phnum
movzx ecx, cx ;/
mov esi, [BaseOfKernelFilePhyAddr + 1Ch] ; esi <- pELFHdr->e_phoff
add esi, BaseOfKernelFilePhyAddr;esi<-OffsetOfKernel+pELFHdr->e_phoff
.Begin:
mov eax, [esi + 0]
cmp eax, 0 ; PT_NULL
jz .NoAction
push dword [esi + 010h] ;size ;`.
mov eax, [esi + 04h] ; |
add eax, BaseOfKernelFilePhyAddr; | memcpy((void*)(pPHdr->p_vaddr),
push eax ;src ; | uchCode + pPHdr->p_offset,
push dword [esi + 08h] ;dst ; | pPHdr->p_filesz;
call MemCpy ; |
add esp, 12 ;/
.NoAction:
add esi, 020h ; esi += pELFHdr->e_phentsize
dec ecx
jnz .Begin
ret
; InitKernel ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
现在就向内核转移了,为什么入口就是0x30400而不是其他的呢??
应该不是随便指定的数据,的确不是随便指定的数据,前面章节我们存放Loader.bin 和Kernel.bin 位置也不是随便指定的数据! 看一下内核加载之后内存的使用情况,你可能就明白了!
如图所示就是内存使用的分布图示:
从这我们可以看出,我们一直用来显示的以0xB8000为开始的内存,就不能被os用在常规用途,在比如0x400 - 0x4FF 这段内存,里面存放了许多参数,也不能覆盖!
但观看到9FC00h这个数字的时候,通过终端15h得到的内存信息已经明确告诉我们,09FC00h ~ 09FFFFh 这段内存不能用作常规使用!即便0h ~ 09FBFFh 可以被使用,仍然把BIOS 参数区保护起来备用!
所以我们真正可以使用的内促是0500 ~ 09FBFFh这一段。那么,为什么指定的入口地址是0x30400离0x500还那么远呢? 其实,之所市这么做是为了调试方便,因为dos都不占用0x30000以上的内存地址,把内核加载到这里,即便在Dos下调试也不会覆盖掉Dos内存。
所以,从0x90000开始的63kb留给了Loader.bin ,0x80000开始的64kb留给了Kernel.bin,0x30000开始的320kb留给了整理后的内核,而页目录和页表被放置在了1MB以上的内存空间。
我们为Loader.bin 留了63kb的空间! 一方面,因为它本质上是个.COM文件,另外一方面我们在写boot.asm 把文件加载了同一个段,文件再大也是不允许的,而且,一个Loader也不会有那么大,所以63kb足够使用了!
加载文件Kernel.bin 到内存的使用方法跟加载Loader.bin 是一样的!也是放在一个段中,所以他也不能超过64kb。不过,暂时来讲,我们的内核还没有那么大,可以作为权宜之计!
向内核交出控制权
下面向内核跳转!
;***************************************************************
jmp SelectorFlatC:KernelEntryPointPhyAddr ; 正式进入内核 *
;***************************************************************
KernelEntryPointPhyAddr的值是0x30400. 跟ld参数 -Ttext指定的值是一致的!若以后更改内核的位置,就改两个地方就行了!
看到一个K 就说明内核已经在执行了!
如图所示,cs ds es fs ss 表示的段统统指向内存地址0h, gs表示的段指向显存,这个是进入保护模式设置的!esp GDT等内容也在Loader中,下面对内核扩充的时候,都会挪到内核中,方便控制!
切换堆栈和GDT
现在我们可以使用c语言了!
代码如下:
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; kernel.asm
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; Forrest Yu, 2005
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; ----------------------------------------------------------------------
; 编译连接方法:
; $ rm -f kernel.bin
; $ nasm -f elf -o kernel.o kernel.asm
; $ nasm -f elf -o string.o string.asm
; $ nasm -f elf -o klib.o klib.asm
; $ gcc -c -o start.o start.c
; $ ld -s -Ttext 0x30400 -o kernel.bin kernel.o string.o start.o klib.o
; $ rm -f kernel.o string.o start.o
; $
; ----------------------------------------------------------------------
SELECTOR_KERNEL_CS equ 8
; 导入函数
extern cstart
; 导入全局变量
extern gdt_ptr
[SECTION .bss]
StackSpace resb 2 * 1024
StackTop: ; 栈顶
[section .text] ; 代码在此
global _start ; 导出 _start
_start:
; 此时内存看上去是这样的(更详细的内存情况在 LOADER.ASM 中有说明):
; ┃ ┃
; ┃ ... ┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■Page Tables■■■■■■┃
; ┃■■■■■(大小由LOADER决定)■■■■┃ PageTblBase
; 00101000h ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■Page Directory Table■■■■┃ PageDirBase = 1M
; 00100000h ┣━━━━━━━━━━━━━━━━━━┫
; ┃□□□□ Hardware Reserved □□□□┃ B8000h ← gs
; 9FC00h ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■■LOADER.BIN■■■■■■┃ somewhere in LOADER ← esp
; 90000h ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■■KERNEL.BIN■■■■■■┃
; 80000h ┣━━━━━━━━━━━━━━━━━━┫
; ┃■■■■■■■■KERNEL■■■■■■■┃ 30400h ← KERNEL 入口 (KernelEntryPointPhyAddr)
; 30000h ┣━━━━━━━━━━━━━━━━━━┫
; ┋ ... ┋
; ┋ ┋
; 0h ┗━━━━━━━━━━━━━━━━━━┛ ← cs, ds, es, fs, ss
;
;
; GDT 以及相应的描述符是这样的:
;
; Descriptors Selectors
; ┏━━━━━━━━━━━━━━━━━━┓
; ┃ Dummy Descriptor ┃
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃ DESC_FLAT_C (0~4G) ┃ 8h = cs
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃ DESC_FLAT_RW (0~4G) ┃ 10h = ds, es, fs, ss
; ┣━━━━━━━━━━━━━━━━━━┫
; ┃ DESC_VIDEO ┃ 1Bh = gs
; ┗━━━━━━━━━━━━━━━━━━┛
;
; 注意! 在使用 C 代码的时候一定要保证 ds, es, ss 这几个段寄存器的值是一样的
; 因为编译器有可能编译出使用它们的代码, 而编译器默认它们是一样的. 比如串拷贝操作会用到 ds 和 es.
;
;
; 把 esp 从 LOADER 挪到 KERNEL
mov esp, StackTop ; 堆栈在 bss 段中
sgdt [gdt_ptr] ; cstart() 中将会用到 gdt_ptr
call cstart ; 在此函数中改变了gdt_ptr,让它指向新的GDT
lgdt [gdt_ptr] ; 使用新的GDT
;lidt [idt_ptr]
jmp SELECTOR_KERNEL_CS:csinit
csinit: ; “这个跳转指令强制使用刚刚初始化的结构”——<<OS:D&I 2nd>> P90.
push 0
popfd ; Pop top of stack into EFLAGS
hlt
代码用简单的4个语句就完成切换堆栈和更换GDT 的任务, 其中,stacktop定义在.bss段中,堆栈大小为2kb。操作GDT用到的gdt_ptr和cstart分别是一个全局变量和全局函数,他们定义在start.c中!
start.c
/*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
start.c
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Forrest Yu, 2005
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
#include "type.h"
#include "const.h"
#include "protect.h"
PUBLIC void* memcpy(void* pDst, void* pSrc, int iSize);
PUBLIC void disp_str(char * pszInfo);
PUBLIC u8 gdt_ptr[6]; /* 0~15:Limit 16~47:Base */
PUBLIC DESCRIPTOR gdt[GDT_SIZE];
PUBLIC void cstart()
{
disp_str("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
"-----\"cstart\" begins-----\n");
/* 将 LOADER 中的 GDT 复制到新的 GDT 中 */
memcpy(&gdt, /* New GDT */
(void*)(*((u32*)(&gdt_ptr[2]))), /* Base of Old GDT */
*((u16*)(&gdt_ptr[0])) + 1 /* Limit of Old GDT */
);
/* gdt_ptr[6] 共 6 个字节:0~15:Limit 16~47:Base。用作 sgdt/lgdt 的参数。*/
u16* p_gdt_limit = (u16*)(&gdt_ptr[0]);
u32* p_gdt_base = (u32*)(&gdt_ptr[2]);
*p_gdt_limit = GDT_SIZE * sizeof(DESCRIPTOR) - 1;
*p_gdt_base = (u32)&gdt;
}
ctart() 首先把位于Loader中的原GDT全部复制给新的GDT中,然后把gdt_ptr中的内容换成新的GDT的基地址和界限。复制GDT使用的函数是memcpy。该函数体放在string.asm!
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; string.asm
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; Forrest Yu, 2005
; ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[SECTION .text]
; 导出函数
global memcpy
; ------------------------------------------------------------------------
; void* memcpy(void* es:pDest, void* ds:pSrc, int iSize);
; ------------------------------------------------------------------------
memcpy:
push ebp
mov ebp, esp
push esi
push edi
push ecx
mov edi, [ebp + 8] ; Destination
mov esi, [ebp + 12] ; Source
mov ecx, [ebp + 16] ; Counter
.1:
cmp ecx, 0 ; 判断计数器
jz .2 ; 计数器为零时跳出
mov al, [ds:esi] ; ┓
inc esi ; ┃
; ┣ 逐字节移动
mov byte [es:edi], al ; ┃
inc edi ; ┛
dec ecx ; 计数器减一
jmp .1 ; 循环
.2:
mov eax, [ebp + 8] ; 返回值
pop ecx
pop edi
pop esi
mov esp, ebp
pop ebp
ret ; 函数结束,返回
; memcpy 结束-------------------------------------------------------------
Loader.asm中显示P 和 K 都删除了,新增加了Kliba.asm – 第三章的内容!
start.c 里面已经增加了
disp_str("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
"-----\"cstart\" begins-----\n");
makefile
##################################################
# Makefile
##################################################
BOOT:=boot.asm
LDR:=loader.asm
KERNEL:=kernel.asm
BOOT_BIN:=$(subst .asm,.bin,$(BOOT))
LDR_BIN:=$(subst .asm,.bin,$(LDR))
KERNEL_BIN:=$(subst .asm,.bin,$(KERNEL))
IMG:=a.img
FLOPPY:=/mnt/floppy/
.PHONY : everything
everything : $(BOOT_BIN) $(LDR_BIN) $(KERNEL_BIN)
ld -m elf_i386 -s -Ttext 0x30400 -o kernel.bin kernel.o string.o start.o kliba.o
dd if=$(BOOT_BIN) of=$(IMG) bs=512 count=1 conv=notrunc
sudo mount -o loop $(IMG) $(FLOPPY)
sudo cp $(LDR_BIN) $(FLOPPY) -v
sudo cp $(KERNEL_BIN) $(FLOPPY) -v
sudo umount $(FLOPPY)
clean :
rm -f $(BOOT_BIN) $(LDR_BIN) $(KERNEL_BIN) *.o
$(BOOT_BIN) : $(BOOT)
nasm $< -o $@
$(LDR_BIN) : $(LDR)
nasm $< -o $@
$(KERNEL_BIN) : $(KERNEL) start.c string.asm
nasm -f elf -o $(subst .asm,.o,$(KERNEL)) $<
nasm -f elf -o string.o string.asm
nasm -f elf -o kliba.o kliba.asm
gcc -m32 -c -fno-builtin -o start.o start.c
编译启动后界面如下:
整理文件夹
- boot.asm 和 Loader.asm放在单独的目录/boot中,包括头文件!
- klib.asm和string.asm放在/lib中,作为库的形象出现!
- kernel.asm和start.c放在/kernel里面。
结构就清晰了!
tree
.
|– a.img
|– bochsrc
|– boot
| |–boot.asm
| |–include
| | |–fat12hdr.inc
| | |–load.inc
| | --pm.inc
|
– loader.asm
|– include
| |– const.h
| |– protect.h
| -- type.h
|-- kernel
| |-- kernel.asm
|
–start.c
-- lib
|-- klib.asm
–string.asm
makefile
makefile 网上讲的很详细,当然书上也有将的比较细致!
就是文件多的话,make需要使用参数-f 指定使用makefile.boot,而不是默认使得makefile或者GNUmakefile!
gcc也增加了制定头文件目录 “-I include”。
#########################
# Makefile for Orange'S #
#########################
# Entry point of Orange'S
# It must have the same value with 'KernelEntryPointPhyAddr' in load.inc!
ENTRYPOINT = 0x30400
# Offset of entry point in kernel file
# It depends on ENTRYPOINT
ENTRYOFFSET = 0x400
# Programs, flags, etc.
ASM = nasm
DASM = ndisasm
CC = gcc
LD = ld
ASMBFLAGS = -I boot/include/
ASMKFLAGS = -I include/ -f elf
CFLAGS = -I include/ -m32 -c -fno-builtin
LDFLAGS = -s -Ttext $(ENTRYPOINT)
DASMFLAGS = -u -o $(ENTRYPOINT) -e $(ENTRYOFFSET)
# This Program
ORANGESBOOT = boot/boot.bin boot/loader.bin
ORANGESKERNEL = kernel.bin
OBJS = kernel/kernel.o kernel/start.o lib/kliba.o lib/string.o
DASMOUTPUT = kernel.bin.asm
# All Phony Targets
.PHONY : everything final image clean realclean disasm all buildimg
# Default starting position
everything : $(ORANGESBOOT) $(ORANGESKERNEL)
all : realclean everything
final : all clean
image : final buildimg
clean :
rm -f $(OBJS)
realclean :
rm -f $(OBJS) $(ORANGESBOOT) $(ORANGESKERNEL)
disasm :
$(DASM) $(DASMFLAGS) $(ORANGESKERNEL) > $(DASMOUTPUT)
# We assume that "a.img" exists in current folder
buildimg :
dd if=boot/boot.bin of=a.img bs=512 count=1 conv=notrunc
sudo mount -o loop a.img /mnt/floppy/
sudo cp -fv boot/loader.bin /mnt/floppy/
sudo cp -fv kernel.bin /mnt/floppy
sudo umount /mnt/floppy
boot/boot.bin : boot/boot.asm boot/include/load.inc boot/include/fat12hdr.inc
$(ASM) $(ASMBFLAGS) -o $@ $<
boot/loader.bin : boot/loader.asm boot/include/load.inc \
boot/include/fat12hdr.inc boot/include/pm.inc
$(ASM) $(ASMBFLAGS) -o $@ $<
$(ORANGESKERNEL) : $(OBJS)
$(LD) -m elf_i386 $(LDFLAGS) -o $(ORANGESKERNEL) $(OBJS)
kernel/kernel.o : kernel/kernel.asm
$(ASM) $(ASMKFLAGS) -o $@ $<
kernel/start.o : kernel/start.c include/type.h include/const.h include/protect.h
$(CC) $(CFLAGS) -o $@ $<
lib/kliba.o : lib/kliba.asm
$(ASM) $(ASMKFLAGS) -o $@ $<
lib/string.o : lib/string.asm
$(ASM) $(ASMKFLAGS) -o $@ $<
sudo make image
添加中断处理
如果要实现进程,需要一种控制权转换机制,就是中断!
之前提到了8259A和建立IDT 中断!
设置一个函数设置8259A
这些基本都是c语言代码了
init_8259A()
{
/* Master 8259, ICW1. */
out_byte(INT_M_CTL, 0x11);
...
}
...
详细看源码,这个c语言基础有啥好说的,自己直接看!
这个c语言跟前面的汇编代码是一样的效果!
其中调用的 out_byte 是汇编语言函数! 还有in_byte
因为端口操作可能需要时间,多加了nop延时!
; ========================================================================
; void out_byte(u16 port, u8 value);
; ========================================================================
out_byte:
mov edx, [esp + 4] ; port
mov al, [esp + 4 + 4] ; value
out dx, al
nop ; 一点延迟
nop
ret
; ========================================================================
; u8 in_byte(u16 port);
; ========================================================================
in_byte:
mov edx, [esp + 4] ; port
xor eax, eax
in al, dx
nop ; 一点延迟
nop
ret
因为函数越来越多,增加了好几个头文件,比如proto.h 用来存放函数声明,可以看到start.c 中 函数 disp_str的声明也放在里面了。memcpy 函数也放在一个头文件里面了,这个头文件是新建立的,取名为string.h!
自然也要修改makefile,有个问题就是头文件越来越多,gcc提供了一个参数 “-M” 可以自动生成依赖关系
gcc -M kernel/start.c -I include
下一步初始化IDT 在start.c中!
EXTERN 定义在const.h中,在global.h 中,如果GLOBAL_VARIABLES_HERE被定义的话,EXTERN会被定义为空值!
start.c 修改后,在kernel添加两句,导入idt_ptr这个符号并加载IDT!
中断或异常发生时 eflags、cs、eip已经被压栈,如果有错误码的话,也会被压栈! 所以对异常处理的总体思想就是,如果有错误码,则直接把向量号压栈,然后执行一个函数exception_handler,如果没有错误码,则先在栈中压入一个0xFFFFFFFF,在把向量号压栈并随后执行exception_handler。
由于C调用约定是调用者恢复堆栈!不用担心excepttion_handler会破坏堆栈中的eip、cs以及eflags!
为了突出显示,excepttion_handler函数使用disp_color_str(),多一个增加设置颜色的函数!!
disp_int是将整数转换成字符串显示出来!
代码自己详细看!
有了异常处理函数,就设置IDT,把IDT代码放进init_port(),也位于protect.c中。
protect.c 只有调用一个函数,init_idt_desc(),它用来初始化一个门描述符。其中用到的函数指针类型是这样定义的
typedef void (*int_handler) ();
所有的一场处理函数都必须与此声明完全一致!
在init_port()中,所有的描述符都被初始化中断门。函数总用到了若干个宏,INT_VECTOR_开头的宏表示中断向量,DA_386IGATE表示中断门,在protect.h中定义,PROVILEGE_KRNL和PRIVILEGE_USER定义在const.h中!
init_8259A()语句也放在这个函数中!
详细请看代码!
start.c
调用inti_port() 修改makefile
ud2 指令用来产生一个#UD异常!
可以在kernel.asm 添加一条指令
csinit:
ud2
初始化8259A和设置IDT 这两项任务,目的是为了有异常处理机制!
8259A中断例程可以看书上代码!
所有的中断都会触发一个函数spurious_irq() 这个函数定义如下
/*======================================================================*
spurious_irq
*======================================================================*/
PUBLIC void spurious_irq(int irq)
{
disp_str("spurious_irq: ");
disp_int(irq);
disp_str("\n");
}
看的出来就是打印IRQ号而已!
设置IDT,还是在protect.c中!
后面我们要设置IF位,所以要修改一下,打开键盘中断!
/* Master 8259, OCW1. */
out_byte(INT_M_CTLMASK, 0xFD);
/* Slave 8259, OCW1. */
out_byte(INT_S_CTLMASK, 0xFF);
我们写入了FD - 1111 1101,于是键盘中断被打开,其他中断仍然处于屏蔽状态。最后在kernel.asm中添加sti指令设置IF 位!
csint:
sti
hlt
make,运行,当我们敲击键盘就会出现spurious_irq:0x1
表面IRQ号就是1,对应就是键盘中断!
字符串输出函数disp_str有bug会导致异常
本人使用代码第5章节f节调试代码,用随书的代码发现在cstart.c 中调用同一个disp_str 函数中,第一个显示正常,第二个显示乱码,本能反应应该是堆栈没有保护,看了源代码,并且对照第三章节的代码,发现代码对寄存器esi ax bi bx都进行了操作,但是只保存ebp进程寄存器,所以,增加了
push esi
push edi
push eax
push ebx
...
pop ebx
pop eax
pop edi
pop esi
经过调试,发现不显示了!!!!这太奇怪了,感觉这个数很古老,尝试网上查找资料,发现是在这段代码不一致:
mov esi, [ebp + 24] ; pszInfo
而原书的代码是
mov esi, [ebp + 8] ; pszInfo
感谢作者!https://blog.csdn.net/w1300048671/article/details/79700831
h\lib\kliba.asm 附上代码
; ========================================================================
; void disp_str(char * info);
; ========================================================================
disp_str:
push ebp
push esi
push edi
push eax
push ebx
mov ebp, esp
mov esi, [ebp + 24] ; pszInfo call压栈占用了4个字节,压入5个寄存器,一个寄存器32位,4字节也就是共20字节 20+4 =24
mov edi, [disp_pos]
mov ah, 0Fh
.1:
lodsb
test al, al
jz .2
cmp al, 0Ah ; 是回车吗?
jnz .3
push eax
mov eax, edi
mov bl, 160
div bl
and eax, 0FFh
inc eax
mov bl, 160
mul bl
mov edi, eax
pop eax
jmp .1
.3:
mov [gs:edi], ax
add edi, 2
jmp .1
.2:
mov [disp_pos], edi
pop ebx
pop eax
pop edi
pop esi
pop ebp
ret
bug修改后的代码 h\lib\kliba.asm
; ========================================================================
; void disp_color_str(char * info, int color);
; ========================================================================
disp_color_str:
push ebp
push esi
push edi
push eax
push ebx
mov ebp, esp
mov esi, [ebp + 24] ; pszInfo
mov edi, [disp_pos]
mov ah, [ebp + 28] ; color
.1:
lodsb
test al, al
jz .2
cmp al, 0Ah ; 是回车吗?
jnz .3
push eax
mov eax, edi
mov bl, 160
div bl
and eax, 0FFh
inc eax
mov bl, 160
mul bl
mov edi, eax
pop eax
jmp .1
.3:
mov [gs:edi], ax
add edi, 2
jmp .1
.2:
mov [disp_pos], edi
pop ebx
pop eax
pop edi
pop esi
pop ebp
ret
遇到另外一个问题就是mount mount: /dev/loop0: can't read superblock错误
网上查询的解决办法:
https://blog.csdn.net/xiaoyi39/article/details/81094747
按照书本的内容执行sudo mount -o loop boot.img /mnt/floppy命令时,可能遇到mount: /dev/loop0: can’t read superblock错误。
解决方式如下:sudo mount -t msdos -o loop boot.img /mnt/floppy
尝试了一次,又出现另外一个问题,就是no space left on device!
怀疑是a.img文件的问题,所以直接把第一章的a.img 直接拷贝过来,在编译一下,ok ,问题解决!