内核使用c语言编写,使用gcc编译ios
ELF文件是指可执行廉洁个事,最初由UNIX系统实验室做为应用程序二进制接口开发和发行的shell
EFL文件类型:bash
程序中又很短的段,如代码段,数据段等,一样也有不少节,段是由节来组成的多个节通过链接过程,被合并成为一个段.函数
跳oop
首先须要一个保护模式下,读取磁盘数据的函数,将内核的可执行文件的内容拷贝到内存.ui
而后须要一个函数,解析可执行文件的内容,将不一样段中的内容,根据表头中定义的地址,拷贝到指定的地址上指针
最后,jmp到入口地址执行便可code
与实模式下的几乎彻底相同,惟一的区别是,在将读取的内容写入内存的时候,使用的是32位寄存器,不在是16位寄存器.接口
; 功能:保护模式下读取硬盘n个扇区 ; 参数: ; eax:开始读取的磁盘扇区 ; cx:读取的扇区个数 ; bx:数据送到内存中的起始位置 rd_disk_m_32: ; 这里要保存eax 的缘由在与,下面section count 寄存器须要一个8位的寄存器 ; 只有acbd这四个寄存器可以拆分为高低8位来使用,而dx做为寄存器号,被占用了 ; 所以须要个abc三个寄存器中一个来用,这里选择了 ax mov esi,eax mov di,cx ; 0x1f2 寄存器:sector count ,读写的时候都表示要读写的扇区数目 ; 该寄存器是8位的,所以送入的数据位 cl mov dx,0x1f2 mov al,cl out dx,al ; 恢复eax mov eax,esi ; eax中存放的是要读取的扇区开始标号,是一个32位的值,所以 al 是低8位 ; 0x1f3 存放0~7位的LBA地址,该寄存器是一个8位的 mov dx,0x1f3 out dx,al ; 下面的 0x1f4 和5 分别是8~15,16~23位LBA地址,这俩寄存器都是8位的 ; 所以是用shr,将eax右移8位,而后每次都用al取eax中的低8位 mov cl,8 shr eax,cl mov dx,0x1f4 out dx,al shr eax,cl mov dx,0x1f5 out dx,al ; 0x1f6 寄存器低4位存放 24~27位LBA地址, ; 0x1f6 寄存器是一个杂项,其第六位,1标识LBA地址模式,0标识CHS模式 ; 上面使用的是LBA地址,所以第六位位1 shr eax,cl and al,0x0f or al,0xe0 mov dx,0x1f6 out dx,al ; 0x1f7 寄存器,读取该寄存器的时候,其中的数据是磁盘的状态 ; 写到该寄存器的时候,写入的僵尸要执行的命令,写入之后,直接开始执行命令 ; 所以须要在写该寄存器的时候,将全部参数设置号 ; 0x20 表示读扇区,0x30写扇区 mov dx,0x1f7 mov al,0x20 out dx,al .not_ready: ; 读 0x1f7 判断数据是否就绪,没就绪就循环等待. nop in al,dx and al,0x88 cmp al,0x08 jnz .not_ready ; 到这一步表示数据就绪,设置各项数据,开始读取 ; 一个扇区512字节,每次读2字节,所以读一个扇区须要256次从寄存器中读取数据 ; di 中是最开始的cx也就是要读取的扇区数 ; mul dx 是ax=ax * dx ,所以最终ax 中是要读取的次数 mov ax,di mov dx,256 mul dx mov cx,ax ; 0x1f0 寄存器是一个16位寄存器,读写的时候,都是数据. mov dx,0x1f0 .go_on_read: in ax,dx mov [ebx],ax ;区别在这里,使用的是ebx add ebx,2 ;ax 是 16位寄存器,读出的也是2字节,所以读一次 dx+2 loop .go_on_read ret
在.go_on_read
中,使用的是[ebx]
不在是实模式下的bx
,由于保护模式下,寻址,须要32位的寄存器,不能再使用16位寄存器进程
为了之后便于维护,如今将文件归类:
文件结构:
└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 05c
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.asm
│ └── mbr.asm
├── build
└── start.sh
添加和加载内核文件,解析内核文件,以及跳转执行内核的常数的宏
; -------------------------------------loader.bin ------------------------------------- ; 将要加载在内存的位置,和在虚拟磁盘的扇区位置 LOADER_IN_MEM equ 0x900 LOADER_IN_DISK equ 2 ; loader 执行的栈基址 LOADER_STACK_TOP equ LOADER_IN_MEM ; -------------------------------------loader.bin ------------------------------------- ; ------------------------------------构造GDT须要的数据 ---------------------------------- ; 16位为一组,最后经过位操做拼接. GDT_48_G_4K equ 00000000_10000000b GDT_48_D_32 equ 00000000_01000000b GDT_48_L_32 equ 00000000_00000000b GDT_48_AVL equ 00000000_00000000b GDT_48_LEN_H equ 00000000_00001111b GDT_32_P equ 10000000_00000000b GDT_32_DPL_0 equ 00000000_00000000b GDT_32_DPL_3 equ 01100000_00000000b GDT_32_S_SYS equ 00000000_00000000b GDT_32_S_USER equ 00010000_00000000b GDT_32_TYPE_CODE equ 00001000_00000000b GDT_32_TYPE_DATA equ 00000010_00000000b ; -----------------------------------构造GDT须要的数据 ---------------------------------- ; -----------------------------------三个段描述符标表项 ---------------------------------- GDT_CODE_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+GDT_48_LEN_H)<<16)+ \ (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_CODE+00000000b) GDT_DATA_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+GDT_48_LEN_H)<<16)+ \ (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_DATA+00000000b) GDT_VGA_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+00000000_00000000b)<<16)+ \ (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_DATA+00001011b) GDT_BASE equ 00000000b<<(24+32) GDT_CODE equ (GDT_CODE_H32<<32)+0x0000FFFF GDT_DATA equ (GDT_DATA_H32<<32)+0x0000FFFF GDT_VGA equ (GDT_VGA_H32<<32 )+0x80000007 ; -----------------------------------三个段描述符标表项 ---------------------------------- ; -----------------------------------构造选择子须要的数据 ---------------------------------- SELECT_RPL_0 equ 00b SELECT_RPL_3 equ 11b SELECT_TI_GDT equ 000b SELECT_TI_LDT equ 100b ; -----------------------------------构造选择子须要的数据 ---------------------------------- ; ----------------------------------- 分页机制 ---------------------------------- PAGE_DIR_TABLE_POS equ 0x100000 PAGE_P equ 1b PAGE_RW_R equ 00b PAGE_RW_W equ 10b PAGE_US_S equ 000b PAGE_US_U equ 100b ; ----------------------------------- 分页机制 ---------------------------------- ; ----------------------------------- 内核加载 ---------------------------------- KERNEL_BIN_IN_MEM equ 0x70000 KERNEL_BIN_IN_DISK equ 0x9 KERNEL_ENTRY equ 0xc0001500 ELF_PT_NULL equ 0 ; ----------------------------------- 内核加载 ----------------------------------
该文件无变化.
该文件变更较大:
kernel_init
函数用来解析内存中的ELF文件,而后将各个段复制到指定的内存位置,所以新加了两个函数,一个kernel_init
和memcpy
jmp
一共新添加了3段代码.
%include "boot.inc" SECTION loader vstart=LOADER_IN_MEM ; 上来就跳转 jmp 0:loader_start gdt_base: dq GDT_BASE gdt_code: dq GDT_CODE gdt_data: dq GDT_DATA gdt_vga: dq GDT_VGA gdt_size equ $-gdt_base ; 这里预留出 60 个段描述符的位置 times 60 dq 0 ; 界限,也就是全局段描述符表在内存中的地址位:gdt_base + 界限 ; 所以界限=长度-1 gdt_ptr dw (gdt_size-1) dd gdt_base ;构建选择子 select_code equ (0x1<<3)+SELECT_TI_GDT+SELECT_RPL_0 select_data equ (0x2<<3)+SELECT_TI_GDT+SELECT_RPL_0 select_vag equ (0x3<<3)+SELECT_TI_GDT+SELECT_RPL_0 ards_buf times 240 db 0 ; ARDS结构,这里定义了240/20个 mem_total dd 0 ; 存放最终结果的内存区域 ards_counts dw 0 ; 最后须要找到全部的ards结构中长度最大的那个内存最为物理内存,所以须要一个遍历 loader_start: ; --------------------------------获取物理内存大小-------------------------------- xor ebx,ebx ;初始ebx为0,告诉bios从头开始遍历每种类型的内存,后续该寄存器的值由bios更新,由bios使用 mov edx,0x534d4150 mov di,ards_buf .get_mem_size_e820: mov eax,0x0000e820 ; 子功能号 mov ecx,20 ; 每次填充的大小,相似于指示可用buf区大小的意思 int 0x15 add di,cx ; di中保存的是buf inc word [ards_counts] ;计数 cmp ebx,0 jnz .get_mem_size_e820 ; 在全部的ards结构中找内存最大值 mov ebx,ards_buf ;ebx 中存放的是地址 mov ecx,[ards_counts] ;ecx 中存放的是次数,须要取地址 xor edx,edx ;保存当前最大值 .find_max_mem: mov eax,[ebx] add eax,[ebx+8] add ebx,20 cmp edx,eax jge .next_ards mov edx,eax .next_ards: loop .find_max_mem jmp .set_max_mem .set_max_mem: mov [mem_total],edx ;最终结果存放 ; --------------------------------获取物理内存大小-------------------------------- ; --------------------------------打印字符-------------------------------- mov ax,0xb800 mov gs,ax mov byte [gs:1120],'l' mov byte [gs:1121],00000111b mov byte [gs:1122],'o' mov byte [gs:1123],00000111b mov byte [gs:1124],'a' mov byte [gs:1125],00000111b mov byte [gs:1126],'d' mov byte [gs:1127],00000111b mov byte [gs:1128],'e' mov byte [gs:1129],00000111b mov byte [gs:1130],'r' mov byte [gs:1131],00000111b mov byte [gs:1132],'!' mov byte [gs:1133],00000111b ; --------------------------------打印字符-------------------------------- ; --------------------------------开启分段模式-------------------------------- ; 开启A20 in al,0x92 or al,000000010b out 0x92,al ; 开启保护模式 mov eax,cr0 or eax,0x00000001 mov cr0,eax ; 加载GDT lgdt [gdt_ptr] ; --------------------------------开启分段模式-------------------------------- ; 流水线的缘由,要强制刷新一次流水线 ; 这里要用远跳转,由于进入了保护模式了,cs寄存器中存的不在是段基址,而是选择子 jmp select_code:p_mode ; 主动告诉编译器下面的代码按照32位机器码编译 [bits 32] p_mode: ; 注意这里ss段寄存器,也被赋值为 select_data mov ax,select_data mov ds,ax mov es,ax mov ss,ax mov esp,LOADER_STACK_TOP mov ax,select_vag mov gs,ax mov byte [gs:1440],'p' ; ----------------------------------- 加载内核文件到内存 ----------------------------------- ; 加载内核的ELF文件 mov eax,KERNEL_BIN_IN_DISK mov ebx,KERNEL_BIN_IN_MEM mov ecx,200 call rd_disk_m_32 mov byte [gs:1442],'r' ; ----------------------------------- 加载内核文件到内存 ----------------------------------- ; 构建页表目录 call setup_page ; 修改全局描述符表 sgdt [gdt_ptr] mov ebx,[gdt_ptr+2] or dword [ebx+0x18+4],0xc0000000 add dword [gdt_ptr+2],0xc0000000 add esp,0xc0000000 ; 将页表目录放入cr3 mov eax,PAGE_DIR_TABLE_POS mov cr3,eax ; 打开cr0的pg位 mov eax,cr0 or eax,0x80000000 mov cr0,eax ; 从新加载 全局描述符表 lgdt [gdt_ptr] mov byte [gs:1600],'V' ; ----------------------------------- 解析内核文件 ----------------------------------- ; 解析内核文件,病跳转执行 call kernel_init ; 须要设置栈指针 mov esp,0xc009f000 mov byte [gs:1760],'K' jmp KERNEL_ENTRY ; ----------------------------------- 解析内核文件 ----------------------------------- ; ----------------------------------- 构建内核页表目录 ----------------------------------- setup_page: ; 首先使用 clear_page 将页表目录所在的那一页 PAGE_DIR_TABLE_POS 开始 ; 的4K空间清零 mov ecx,4096 mov esi,0 .clear_page: mov byte [PAGE_DIR_TABLE_POS+esi],0 inc esi loop .clear_page ; 将内核页表目录的第一个页表和内核在3G处的页表设为同一个,为了是在一进入分页机制后,虚拟地址映射正常 .create_pde: mov eax,PAGE_DIR_TABLE_POS add eax,4096 ; 页表目录后面紧跟着就是第一个页表 eax+4K, mov ebx,eax ; 所以eax中存的是第一个页表的地址 or eax,PAGE_US_U|PAGE_RW_W|PAGE_P ; eax中是第一个页表的地址,而后设置属性,让他成为第一个页表项 mov [PAGE_DIR_TABLE_POS],eax ; 将第一个页表放在页表目录的第0个表项和高1G内存开始的地方. mov [PAGE_DIR_TABLE_POS+768*4],eax ;768=3*1024*1024 /(4*1024),因此高1G是在第768个页表,*4是由于,每一个表项4字节 sub eax,4096 ; eax是第一个页表的地址,减去4KB,就是 PAGE_DIR_TABLE_POS mov [PAGE_DIR_TABLE_POS+4092],eax ; 将页表目录最后一项设为本身的地址 ; 填充第一个页表 mov ecx,256 mov esi,0 mov edx,PAGE_US_U|PAGE_RW_W|PAGE_P ; edx如今是第一个页表中的第一个表项,他表明的是物理内存的第一页. .create_pte: mov [ebx+esi*4],edx ; 将物理内存从0开始依次映射到第一个页表中 add edx,4096 inc esi loop .create_pte ; 填充高1G的其余页表目录项,页表目录所在内存页的后面页就放着页表,也就是说这些页表,在物理内存上是连续的. mov eax,PAGE_DIR_TABLE_POS add eax,4096*2 ; eax 中如今是第一个页表的地址 or eax,PAGE_US_U|PAGE_RW_W|PAGE_P mov ebx,PAGE_DIR_TABLE_POS mov ecx,254 ;第一个和最后一个已经建立完了,因此是256-2个 mov esi,769 ;是高1G的第2个页表 .create_kernel_pde: mov [ebx,esi*4],eax inc esi add eax,4096 loop .create_kernel_pde ret ; ----------------------------------- 读取磁盘文件 ----------------------------------- ; 功能:保护模式下读取硬盘n个扇区 ; 参数: ; eax:开始读取的磁盘扇区 ; cx:读取的扇区个数 ; bx:数据送到内存中的起始位置 rd_disk_m_32: ; 这里要保存eax 的缘由在与,下面section count 寄存器须要一个8位的寄存器 ; 只有acbd这四个寄存器可以拆分为高低8位来使用,而dx做为寄存器号,被占用了 ; 所以须要个abc三个寄存器中一个来用,这里选择了 ax mov esi,eax mov di,cx ; 0x1f2 寄存器:sector count ,读写的时候都表示要读写的扇区数目 ; 该寄存器是8位的,所以送入的数据位 cl mov dx,0x1f2 mov al,cl out dx,al ; 恢复eax mov eax,esi ; eax中存放的是要读取的扇区开始标号,是一个32位的值,所以 al 是低8位 ; 0x1f3 存放0~7位的LBA地址,该寄存器是一个8位的 mov dx,0x1f3 out dx,al ; 下面的 0x1f4 和5 分别是8~15,16~23位LBA地址,这俩寄存器都是8位的 ; 所以是用shr,将eax右移8位,而后每次都用al取eax中的低8位 mov cl,8 shr eax,cl mov dx,0x1f4 out dx,al shr eax,cl mov dx,0x1f5 out dx,al ; 0x1f6 寄存器低4位存放 24~27位LBA地址, ; 0x1f6 寄存器是一个杂项,其第六位,1标识LBA地址模式,0标识CHS模式 ; 上面使用的是LBA地址,所以第六位位1 shr eax,cl and al,0x0f or al,0xe0 mov dx,0x1f6 out dx,al ; 0x1f7 寄存器,读取该寄存器的时候,其中的数据是磁盘的状态 ; 写到该寄存器的时候,写入的僵尸要执行的命令,写入之后,直接开始执行命令 ; 所以须要在写该寄存器的时候,将全部参数设置号 ; 0x20 表示读扇区,0x30写扇区 mov dx,0x1f7 mov al,0x20 out dx,al .not_ready: ; 读 0x1f7 判断数据是否就绪,没就绪就循环等待. nop in al,dx and al,0x88 cmp al,0x08 jnz .not_ready ; 到这一步表示数据就绪,设置各项数据,开始读取 ; 一个扇区512字节,每次读2字节,所以读一个扇区须要256次从寄存器中读取数据 ; di 中是最开始的cx也就是要读取的扇区数 ; mul dx 是ax=ax * dx ,所以最终ax 中是要读取的次数 mov ax,di mov dx,256 mul dx mov cx,ax ; 0x1f0 寄存器是一个16位寄存器,读写的时候,都是数据. mov dx,0x1f0 .go_on_read: in ax,dx mov [ebx],ax ;区别在这里,使用的是ebx add ebx,2 ;ax 是 16位寄存器,读出的也是2字节,所以读一次 dx+2 loop .go_on_read ret ; ----------------------------------- 读取磁盘文件 ----------------------------------- ; ----------------------------------- 解析内核ELF ----------------------------------- ; 功能:解析内核ELF文件 ; 不须要参数,由于参数是 KERNEL_BIN_IN_MEM ,并且代码只是用一次 ; 所以参数直接写死,不更改 kernel_init: xor eax,eax xor ebx,ebx xor ecx,ecx xor edx,edx ; 偏移42字节处是 e_phentsize是每一个程序头结构的大小 mov dx,[KERNEL_BIN_IN_MEM+42] ; 偏移28字节处是 e_phoff,是第一个 程序头结构在文件中的偏移 mov ebx,[KERNEL_BIN_IN_MEM+28] ; 加上 KERNEL_BIN_IN_MEM ,就是第一个程序头在文件中的偏移地址 add ebx,KERNEL_BIN_IN_MEM ; 偏移44字节处是 e_phnum ,是程序头结构的个数 mov cx,[KERNEL_BIN_IN_MEM+44] ; 遍历每一个程序头结构,按照其 p_offset,加载到最内存的指定位置 .each_segment: ; p_type =0 则该程序头未使用,跳过 cmp byte [ebx+0],ELF_PT_NULL je .pt_null ; 程序头结构的偏移16 字节是 p_filesz 是结构的大小 push dword [ebx+16] ; 程序头结构的偏移16 字节是 p_offset 是该结构在ELF文件中的偏移地址 mov eax,[ebx+4] ; 加上 KERNEL_BIN_IN_MEM add eax,KERNEL_BIN_IN_MEM push eax ; 程序头结构的偏移4 字节是 p_vaddr 是该结构最终 加载在内存中地址 push dword [ebx+8] call memcpy add esp,12 .pt_null: add ebx,edx loop .each_segment ret ; ----------------------------------- 解析内核ELF ----------------------------------- ; ----------------------------------- 批量拷贝 ----------------------------------- memcpy: cld push ebp mov ebp,esp push ecx mov edi,[ebp+8] mov esi,[ebp+12] mov ecx,[ebp+16] rep movsb pop ecx pop ebp ret ; ----------------------------------- 批量拷贝 -----------------------------------
memcpy
函数的解释以下:
MOVSB(MOVe String Byte):即字符串传送指令,这条指令按字节传送数据。经过SI和DI这两个寄存器控制字符串的源地址和目标地址,好比DS:SI这段地址的N个字节复制到ES:DI指向的地址,复制后DS:SI的内容保持不变。
而REP(REPeat)指令就是“重复”的意思,术语叫作“重复前缀指令”,由于既然是传递字符串,则不可能一个字(节)一个字(节)地传送,因此须要有一个寄存器来控制串长度。这个寄存器就是CX,指令每次执行前都会判断CX的值是否为0(为0结束重复,不为0,CX的值减1),以此来设定重复执行的次数。所以设置好CX的值以后就能够用REP MOVSB了。
CLD(CLear Direction flag)则是清方向标志位,也就是使DF的值为0,在执行串操做时,使地址按递增的方式变化,这样便于调整相关段的的当前指针。这条指令与STD(SeT Direction flag)的执行结果相反,即置DF的值为1。
暂时比较简单,直接是一个循环
int main( int argc, char const* argv[] ) { while ( 1 ) { } return 0; }
可是其编译过程不简单:
-m32
参数-e main
将main
函数做为程序的入口,而后指定入口地址-Ttext 0xc0001500
,再而后,由于gcc -c
编译结果位32位程序,所以连接的时候须要带上-m elf_i386
参数所以最终为:
gcc -o kernel.o -m32 -c ./kernel/main.c ld -Ttext 0xc0001500 -m elf_i386 -e main -o kernel.bin ./kernel.o
改动较大,由于改变路目录结构,又新加了文件,所以,须要变更的比较多:
-o
命令,指定输出的路径和文件名mbr.asm
和loader.asm
文件引入的boot.inc
文件移动.所以,在编译的时候要加上-I ./boot/inlucde
main.c
文件的编译,连接,刻录#! /bin/bash # 编译mbr.asm echo "----------nasm mbr.asm----------" if !(nasm -o ./build/mbr.bin ./boot/mbr.asm -I./boot/include/);then echo "nasm error" exit fi # 刻录mbr.bin echo "----------dd mbr.bin ----------" if !(dd if=./build/mbr.bin of=../hd60m.img bs=512 count=1 conv=notrunc);then echo "dd error" exit fi # 编译 loader.asm echo "----------nasm loader.asm----------" if !(nasm -o ./build/loader.bin ./boot/loader.asm -I./boot/include/);then echo "nasm error" exit fi # 刻录loader.bin echo "----------dd loader.bin ----------" if !(dd if=./build/loader.bin of=../hd60m.img bs=512 count=4 seek=2 conv=notrunc);then echo "dd error" exit fi # 编译内核 echo "----------gcc -c kmain.c ----------" if !(gcc -o ./build/kernel.o -m32 -c ./kernel/main.c );then echo "dd error" exit fi # 连接 echo "----------ld kernel.o ----------" if !(ld -Ttext 0xc0001500 -m elf_i386 -e main -o ./build/kernel.bin ./build/kernel.o);then echo "dd error" exit fi # 刻录 kernel.bin echo "----------dd kernel.bin ----------" if !(dd if=./build/kernel.bin of=../hd60m.img bs=512 count=40 seek=9 conv=notrunc);then echo "dd error" exit fi # 删除临时文件 sleep 1s rm -rf ./build/*.* # 运行bochs cd .. bochs
在0x1500
打上断点,执行到后,开跟踪.