编写 hello.asm
[section .data] ; 数据段
strHello db "Hello, world!", 0Ah
STRLEN equ $ - strHello
[section .text] ; 代码段
global _start ; 我们必须导出 _start 这个入口,以便让链接器识别
_start:
mov edx, STRLEN
mov ecx, strHello
mov ebx, 1
mov eax, 4 ; sys_write
int 0x80 ; 系统调用
mov ebx, 0
mov eax, 1 ; sys_exit
int 0x80 ; 系统调用
编译链接
$ nasm -f elf hello.asm -o hello.o
$ ld -s hello.o -o hello # 或许会报错,我这里是 ld -m elf_i386 -s hello.o -o hello
./hello
必须要定义入口点,且要通过 global 关键字将它导出,这样链接程序才能找到它。
foo.asm
extern choose ; extern 声明本文件外的函数
; int choose(int a, int b)
[section .data] ; 数据段
num1st dd 3
num2nd dd 4
[section .text] ; 代码段
global _start ; 导出入口点,以便链接器识别
global myprint ; 导出这个函数,便于让 c 调用
_start:
; 调用 choose(num1st, num2nd)
push dword [num2nd]
push dword [num1st]
call choose
add esp, 8
mov ebx, 0
mov eax, 1 ; sys_write
int 0x80 ; 系统调用
; void myprint(char* msg, int len)
myprint:
mov edx, [esp + 8] ; len
mov ecx, [esp + 4] ; msg
mov ebx, 1
mov eax, 4 ; sys_write
int 0x80 ; 系统调用
ret
bar.c
void myprint(char* msg, int len);
int choose(int a, int b) {
if(a >= b)
myprint("the 1st one\n", 13);
else
myprint("the 2nd one\n", 13);
return 0;
}
编译链接
$ nasm -f elf -o foo.o foo.asm
$ gcc -c -o bar.o bar.c # 若平台是 64 位,则默认会以 64 位生成目标文件
# $ gcc -m32 -c bar.c -o bar.o # 上面一条的解决方案,指定 32 位生成目标文件
$ ld -m elf_i386 -s -o foobar foo.o bar.o # 指定架构生成
./foobar
注意:
在计算机科学中,是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件的文件格式。
ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。
#define EI_NIDENT 16
typedef struct{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
数据类型 | 大小 | 对齐 | 用途 |
---|---|---|---|
Elf32_Addr | 4 | 4 | 无符号程序地址 |
Elf32_Half | 2 | 2 | 无符号中等大小整数 |
Elf32_Off | 4 | 4 | 无符号文件偏移 |
Elf32_Sword | 4 | 4 | 有符号大整数 |
Elf32_Word | 4 | 4 | 无符号大整数 |
unsigned char | 1 | 1 | 无符号小整数 |
最开头是16个字节的e_ident, 其中包含用以表示ELF文件的字符,以及其他一些与机器无关的信息。开头的4个字节值固定不变,为0x7f和ELF三个字符。
e_type
:它标识的是该文件的类型。e_machine
:表明运行该程序需要的体系结构。e_version
:表示文件的版本。e_entry
:程序的入口地址。e_phoff
:表示Program header table 在文件中的偏移量(以字节计数)。e_shoff
:表示Section header table 在文件中的偏移量(以字节计数)。e_flags
:对IA32而言,此项为0。e_ehsize
:表示ELF header大小(以字节计数)。e_phentsize
:表示Program header table中每一个条目的大小。e_phnum
:表示Program header table中有多少个条目。e_shentsize
:表示Section header table中的每一个条目的大小。e_shnum
:表示Section header table中有多少个条目。e_shstrndx
:包含节名称的字符串是第几个节(从零开始计数)。第一行(红线部分):e_type,其中开头四个字节(黄色字体)是固定不变的。
第二行,分别是:e_machine(0x0002)、e_version(0x0003)、e_entry(0x08049000),以此类推。
Program header描述的是一个段在文件中的位置、大小以及它被放进内存后所在的位置和大小。
看前一节的图,得到 Program header table 在文件中的偏移量(e_phoff)为 0x34,而 ELF header 大小(e_ehsize)也是 0x34,可见 ELF header 后面紧跟着就是 Program header table。
typedef struct {
Elf32_Word p_type; // 0
Elf32_Off p_offset; // 4
Elf32_Addr p_vaddr; // 8
Elf32_Addr p_paddr; // 12, 0Ch
Elf32_Word p_filesz; // 14, 0Eh
Elf32_Word p_memsz; // 18, 12h
Elf32_Word p_flags; // 22, 16h
Elf32_Word p_align; // 26, 1Ah
} Elf32_Phdr;
p_type
:当前Program header所描述的段的类型。
p_offset
:段的第一个字节在文件中的偏移。
p_vaddr
:段的一个字节在内存中的虚拟地址
p_paddr
:在物理内存定位相关的系统中,此项是为物理地址保留。
p_filesz
:段在文件中的长度。
p_memsz
:段在内存中的长度。
p_flags
:与段相关的标志。
p_align
:根据此项值来确定段在文件及内存中如何对齐。
这些信息可以帮助我们把文件加载进内存。
Loader 的工作:
加载内核到内存,和加载 Loader 进内存是一样的,区别在于把内核加载进内存需要根据 Program header table 中的值把内核中相应的段放到正确的位置。
代码和之前 Loader 几乎一样,但多了个 KillMotor 函数,作用是关闭软驱马达,不然软驱的灯会一直亮着。
区别:
LABEL_FILENAME_FOUND: ; 找到 KERNEL.BIN 后,便来到这里。
; ...
push eax ;|
mov eax, [es:di + 01Ch] ;| // 这些都是新增加的
mov dword [dwKernelSize], eax ;| // 保存 KERNEL.BIN 的文件大小
pop eax ;|
; ...
LABEL_GOON_LOADING_FILE:
; 关闭软驱马达
KillMotor:
push dx
mov dx, 03F2h
mov al, 0
out dx, al
pop dx
ret
写一个算不上内核的内核雏形:kernel.asm
[section .text] ; 代码段
global _start
_start:
mov ah, 0Fh
mov al, 'K'
mov [gs:((80 * 1 + 39) * 2)], ax
jmp $
编译运行:
$ nasm -f elf -o kernel.o kernel.asm
$ nasm loader.asm -o loader.bin
ld -m elf_i386 -s -o kernel.bin kernel.o
mount -o loop a.img /mnt/floppy/
cp kernel.bin /mnt/floppy/ -v
cp loader.bin /mnt/floppy/ -v
umount /mnt/floppy/
运行效果参考:P131 - 图 5.4
loader.ams —— GDT
; =================================================================
; GDT
; 段地址 段界限 属性
LABEL_GDT: Descriptor 0, 0, 0
LABEL_DESC_FLAT_C: Descriptor 0, 0fffffh, DA_CR | DA_32 | DA_LIMIT_4K
LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, DA_DRW | DA_32 | DA_LIMIT_4K
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW | DA_DPL3
GdtLen equ $ - LABEL_GDT ; 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
; =================================================================
loader.asm 的 32 位代码段 —— 进入保护模式后要执行的代码
; ==================================================================================
; 保护模式
; 32 位代码段,由实模式跳入
[SECTION .s32]
ALIGN 32
[BITS 32]
LABEL_PM_START:
mov ax, SelectorVideo
mov gs, ax ; 初始化 gs 寄存器
; 初始化各个寄存器
mov ax, SelectorFlatRW
mov ds, ax
mov es, ax
mov fs, ax
mov ss, ax
mov esp, TopOfStack
; --------------------------------
; 这一块是做准备启动分页机制的工作
push szMemChkTitle
call DispStr
add esp, 4
call DispMemInfo ; 打印内存信息
call SetupPaging ; 开启分页机制
; /--------------------------------
mov ah, 0Fh ; 黑底白字
mov al, 'P'
mov [gs:((80 * 0 + 39) * 2)], ax ; 屏幕第 0 行,第 39 列
;jmp $
call InitKernel
;**************************************************************
jmp SelectorFlatC:KernelEntryPointPhyAddr ; 正式进入内核
; 这行代码是在完成“重新放置内核”后添加的
;**************************************************************
Loader.asm —— 正式进入保护模式
LABEL_FILE_LOADED:
call KillMotor ; 关闭软驱马达
mov dh, 1 ; "Ready."
call DispStrRealMode ; 显示字符串
; 下面准备跳入保护模式
lgdt [GdtPtr] ; 加载 GDTR
cli ; 关中断
; 打开地址线 A20
in al, 92h
or al, 00000010b
out 92h, al
; 开启保护模式的开关
mov eax, cr0
or eax, 1
mov cr0, eax
; 跳入保护模式
jmp dword SelectorFlatC:(BaseOfLoaderPhyAddr + LABEL_PM_START)
通过 DispMemInfo 函数(该函数在这里就不放出来了)得到内存信息,接着就开始编写启动分页机制的代码;
;---------------------------------------------------------------
; 函数名: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
xor eax, eax
mov eax, PageTblBase | PG_P | PG_USU | PG_RWW ; 构建 PDE
.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 ; 构建 PTE
.2:
stosd
add eax, 4096 ; 每一页都指向 4K 的空间
loop .2
mov eax, PageDirBase
mov cr3, eax ; 将页目录的物理地址保存到 CR3 寄存器中
mov eax, cr0
or eax, 80000000h ; 设置 CR0 的最高 PG 位为 1,开启分页机制
mov cr0, eax
jmp short .3
.3:
nop
ret
运行结果参考:P138 - 图 5.7
我们的任务:根据内核的 Program header table 的信息将所有的 Program Header 复制到内存的正确位置中。
;---------------------------------------------------------------
; 函数名:InitKernel
;---------------------------------------------------------------
; 功能:将 KERNEL.BIN 的内容经过整理对其后放到新的位置
; 遍历每个 Program Header,根据 Program Header 中的信息
; 来确定把什么放进内存,放到什么位置,以及放多少。
;---------------------------------------------------------------
InitKernel:
xor esi, esi
mov cx, word [BaseOfKernelFilePhyAddr + 2Ch] ; ecx <- ELFheader.e_phnum
movzx ecx, cx ; 将 cx 扩展到 32 位,赋值给 ecx
mov esi, [BaseOfKernelFilePhyAddr + 1Ch] ; esi <- ELFheader.e_phoff
add esi, BaseOfKernelFilePhyAddr ; esi <- OffsetOfKernel + ELFheader.e_phoff
; 此时 esi 以及构成了一个物理地址
.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*)(PHeader.p_vaddr), uchCode + PHeader.p_offset, PHeader.p_filesz)
push eax ; src ; |
push dword [esi + 08h] ; dst ;/
call MemCpy
add esp, 12
.NoAction:
add esi, 020h ; esi += ELFheader.e_phentsize 指向下一个 Program Header
dec ecx
jnz .Begin
ret
Tips: 其中 MemCpy 函数是模仿 C 中的 memcpy() 编写的,用于内存复制。
kernel.asm
SELECTOR_KERNEL_CS equ 8
; 导入函数
extern cstart
; 导入全局变量
extern gdt_ptr
[SECTION .bss]
StackSpace resb 2 * 1024
StackTop: ; 栈顶
[section .text] ; 代码段
global _start ; 导出入口点
_start:
; 把 esp 从 LOADER 挪到 KERNEL
mov esp, StackTop ; 堆栈在 bss 段中
sgdt [gdt_ptr] ; cstart() 中将会用到 gdt_ptr
call cstart ; 在此函数中改变了 gdt_ptr,让它指向新的 GDT
lgdt [gdt_ptr] ; 使用新的 GDT
jmp SELECTOR_KERNEL_CS:csinit
csinit:
push 0
popfd ;
hlt
start.c
#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, (void*)(*((u32*)(&gdt_ptr[2]))), *((u16*)(&gdt_ptr[0])) + 1);
// 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;
}
函数介绍: void *memcpy(void*dest, const void *src, size_t n);
作用: 由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内,是用指针进行操作的。
我懒了…
需要完成的工作:
各种头文件:
const.h
:添加 8259A 的端口。protecth
:添加中断向量、描述符等。proto.h
:函数声明。type.h
:添加函数指针。设置 8259A: kernel\i8259A.c
使用 C 语言完成初始化。
初始化 IDT: start.c
PUBLIC void cstart() {
...
// idt_ptr[6] 共 6 个字节:0~15:Limit 16~47:Base。用作 sgdt/lgdt 的参数。
u16* p_idt_limit = (u16*) (&idt_ptr[0]);
u32* p_idt_base = (u32*) (&idt_ptr[2]);
*p_idt_limit = IDT_SIZE * sizeof(GATE) - 1;
*p_idt_base = (u32) &idt;
...
}
先设置 IDT,然后接下来在 kernel.asm 中加载 IDT。
_start:
...
lidt [idt_ptr] ; 注册 IDT
...
现在 IDT 已经加载完毕,但其内容是空的,因此需要进行填充。
/**
* 初始化 IDT 描述符
* parms:编号、描述符类型、对应的中断例程、特权级
*/
PRIVATE void init_idt_desc(unsigned char vector, u8 desc_type, int_handler handler, unsigned char privilege) {
GATE * p_gate = &idt[vector];
u32 base = (u32) handler;
p_gate -> offset_low = base & 0xFFFF;
p_gate -> selector = SELECTOR_KERNEL_CS;
p_gate -> dcount = desc_type | (privilege << 5);
p_gate -> attr = desc_type | (privilege << 5);
p_gate -> offset_high = (base >> 16) & 0xFFFF;
}
PUBLIC void init_prot() {
// ...
init_idt_desc(INT_VECTOR_IRQ0 + 0, DA_386IGate,
hwint00, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ0 + 1, DA_386IGate,
hwint01, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ0 + 2, DA_386IGate,
hwint02, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ0 + 3, DA_386IGate,
hwint03, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ0 + 4, DA_386IGate,
hwint04, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ0 + 5, DA_386IGate,
hwint05, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ0 + 6, DA_386IGate,
hwint06, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ0 + 7, DA_386IGate,
hwint07, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ8 + 0, DA_386IGate,
hwint08, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ8 + 1, DA_386IGate,
hwint09, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ8 + 2, DA_386IGate,
hwint10, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ8 + 3, DA_386IGate,
hwint11, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ8 + 4, DA_386IGate,
hwint12, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ8 + 5, DA_386IGate,
hwint13, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ8 + 6, DA_386IGate,
hwint14, PRIVILEGE_KRNL);
init_idt_desc(INT_VECTOR_IRQ8 + 7, DA_386IGate,
hwint15, PRIVILEGE_KRNL);
}
查看架构:
$ ld -m elf
ld: 无法辨认的仿真模式: elf
支持的仿真: elf_x86_64 elf32_x86_64 elf_i386 elf_iamcu elf_l1om elf_k1om i386pep i386pe
根据选择对应架构开始链接:
$ ld -m elf_i386 -s hello.o -o hello