二、进入保护模式--内核加载器LOADER:实模式下内存容量检测、开启保护模式、开启分页模式、加载kernel到内存缓冲区、加载kernel到内存(内存复制函数)-kernel

本文主要是介绍二、进入保护模式--内核加载器LOADER:实模式下内存容量检测、开启保护模式、开启分页模式、加载kernel到内存缓冲区、加载kernel到内存(内存复制函数)-kernel,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

保护模式

我们刚开机时候进入的都是实模式,对硬件访问没有任何的保护措施,随意修改里面的程序,及其不安全。所以我们之后进入保护模式。
保护模式:cpu扩展、寻址扩展、运行模式扩展、运行模式反转、指令扩展

CPU扩展:

实模式是使用的8086的CPU的16位,保护模式的运行环境变为了32位。所以开机时32位的CPU先处于16位的状态,再转为32位。但是16位也可以访问32位的资源。16位模式下默认操作数位16位,32位模式下默认操作数位32位

寄存器扩展:

除了段寄存器外,通用寄存器、指令指针寄存器、标志寄存器都从原来的16位扩展到32位。AX、BX、CX、DX、SI、DI、BP、SP-->EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP,FLAGS-->ELAGS,IP-->EIP。

寻址扩展:

实模式下的寻址方式:基址寄存器只能是bx、bp,变址寄存器只能是si、di,bx的默认寄存器是ds、bp的默认寄存器是ss,但是保护模式下,基址寄存器变为所有的32位通用寄存器,变址寄存器变为了除了esp之外的所有32位通用寄存器,偏移量由实模式的16位变为了32位。并且变址寄存器可以乘以一个比例因子,比例因子只能是1、2、4、8。

运行模式反转:

当CPU处于实模式下的16位,依然可以使用32位下的资源,eg:[bits 16] mov eax,0x1234,其中eax是32位的资源,按照常理,CPU操作时候,因为默认16位,所以只会将0x1234读给ax,对eax的前两个字节不管不顾( 保持原值),但是我们的目的是要求CPU也照顾到eax的前两个字节(置0),所以编译器要生成机器码,这需要我们告诉它生成样的机器码,是否要加上前缀来提醒CPU考虑前两个字节。16位下的机器码和32位的机器码是不同的,有时候一个寄存器表示的机器码都不同。所以我们的代码刚开始是在实模式下运行的(默认的),编译器看到32位的资源也会前缀反转去使用它。但是之后我们要进入保护模式32位,关键是还是同一个程序,我们要通知编译器我们进入32位了,看到32资源不需要前缀反转了,所以如下:
[bits 16]是告诉编译器,下面的代码将我编译成16位的机器码,使CPU只考虑16位的资源(寄存器资源、立即数等)。
[bits 32]]是告诉编译器,下面的代码将我编译成32位的机器码,使CPU只考虑32位的资源(寄存器资源、立即数等)。
指令格式:

操作数反转前缀:指令前缀0x66

16位和32位模式之间可以互相使用资源,16位模式下默认操作数位16位,32位模式下默认操作数位32位。但是有时候我们的要求是不一样的,比如要在16位下使用32位的操作数,所以要在机器指令之前加上指令前缀0x66让cpu来识别应该将立即数翻译成0x1234,还是0x0000 1234.
机器码前加上0x66后,假设当前运行模式是16位实模式,操作数大小将变为32位
机器码前加上0x66后,假设当前运行模式是32位保护模式,操作数大小将变为16位
[bits 16] mov eax,0x1234  ;16位模式下按照常规CPU要将0x1234翻译成3412(小端),但eax的32位资源,我们的意思是要考虑eax,所以编译器需要将0x1234反转为32位的34120000。
[bits 32] mov ax,0x1234  ;32位模式下按照常规cpu将0x1234翻译成34120000,但ax是16位资源,我们的意思是只需考虑ax,编译器就翻译为0x3412

寻址方式反转前缀:指令前缀0x67

[bits 16] mov word[eax],0x1234  ;16位模式下,ax是不允许加入到基址寻址中的,编译器将会在机器码前加上67表示段基址加上eax中值将是我们将要访问的内存值。word为伪指令,表示在内存开始出连续写入两个字节大小的数据。
[bits 16] mov dword[eax],0x1234  ;16为模式下,ax是不允许的,所以加上67,但是dword伪指令要在所示内存出连续写入四个字节大小的数据,16位下0x1234默认只翻译位3412,所以还要将3412 0000,所以还要加上66。
[bits 32]mov word [eax],0x1234 ;32位下CPU默认将立即数翻译为32位,但是我们的要求是伪指令word,只要一个字即可,所以加66
[bits 32]mov dword [bx],0x1234  ;32位下CPU默认使用ebx,但是我们的意思是只是用bx,不要使用它的前两个字节,所以加上67。

指令扩展

loop:

 实模式下用cx来储存循环次数,保护模式下要用ecx。每操作一次循环体后cx减1,然后执行loop指令前要拿ecx和0比较(循环条件),等于0则停止循环,不等于0则执行loop指令。相当于for

mul:

实模式下:
如果乘数是8位,则al当作另一个乘数,结果是16位,结果在AX中。
如果乘数是16位,则ax当作另一个乘数,结果是32位,存入高16位在DX中,低16位在AX中。
如果乘数是32位,则eax当作另一个乘数,结果是64位,存入edx:eax中,其中edx是高32位,eax是低32位。
保护模式下:
如果乘数是8位,则al当作另一个乘数,结果是16位,存入ax中。
如果乘数是16位,则ax当作另一个乘数,结果是32位,存入eax中。
如果乘数是32位,则eax当作另一个乘数,结果是64位,存入edx:eax中,其中edx是高32位,eax是低32位。

div:

如果除数是8位,被除数就是16位,位于ax中,结果的商在al,余数在ah
如果除数是16位,被除数就是32位:高16位位于dx,低16位位于ax中,结果的商在ax,余数在dx
如果除数是32位,被除数就是64位:高32位位于edx,低32位位于eax中,结果的商在eax,余数在edx

push:

不管在实模式还是保护模式都可以同时处理16位和32位的数据。

push 立即数

push 寄存器

push 内存

push 立即数:
实模式下:
压入8位立即数,因为默认操作数是16位,CPU位将其扩展为16位后,将其入栈。sp-2

压入16位立即数,cpu会将其直接入栈。sp-2

压入32位立即数,cpu会将其直接入栈。sp-4

保护模式下:

压入8位立即数,因为默认操作数是32位,CPU位将其扩展为32位后,将其入栈,sp-4

压入16位立即数,cpu会将其直接入栈,sp-2

压入32位立即数,cpu会将其直接入栈,sp-4

push 寄存器:
实模式下:

压入段寄存器:cs、ds、es、fs、gs、ss,,按照当前默认操作数大小压入,sp-2

压入通用寄存器,如果压入的16位的,sp-2,如果压入32位的,sp-4

保护模式下:
压入段寄存器:cs、ds、es、fs、gs、ss,s按照当前默认操作数压入,即使段寄存器是16位的,sp-4
压入通用寄存器,如果压入的16位的,sp-2,如果压入32位的,sp-4
push 内存
实模式:

压入的16位数据,sp-2    eg:push word [0x1234]

压入的64位数据,sp-4   eg:push dword [0x1234]

保护模式:

压入的16位数据,sp-2

压入的64位数据,sp-4


大数据复制三剑客:cld、(ecx、esi、edi、),rep  movs[bwd]  ;首先将ecx的值和0比较,不相等则开始复制,一次复制一字节,esi和sdi自动加一字节,然后ecx减1。这个周期完成。继续ecx和0比较,继续。。。.
mov byte/word/dword[ebx],eax
db、dw、dd、dq

获取内存容量:

linux下获取内存的方法:Linux2.6是通过调用detect_memory函数来获取的,其本质是通过调用BIOS中断0x15,利用下面的3个子功能的任何一个来获取内存。子功能从功能复杂到简单分别为:
EAX=0xE820:遍历主机的所有内存
AX=0XE801:分别检测低15MB和16MB-4GB的内存,最大支持4GB。
AH=0X88:最多检测出64MB内存,实际内存超过64MB按照64MB返回。
BIOS中断是在实模式下的方法

BIOS中断0x15子功能0xE820获取内存

0xE820能够获取系统的内存布局,按照内存的类型属性来划分这片内存,BIOS会按照类型的来返回内存信息,即返回地址描述符结构。一般有:系统的ROM,设备内存映射的内存区域、不可用内存、DRAM等。比如我们的内存有6种类型,就会返回6个ARDS到我们指定的内存区域中,到时候我们只需要根据ARDS的各单位的偏移量都出来就行。

对于32位系统来说,最大内存位4G,所以我们检查时候的只看BaseAddLow+LengthLow这两个单元即可。

我们调用BIOS的中断只按要求提供参数即可:

所以步骤为:

1,填好“调用前输入”中列出的寄存器

2,执行中断调用int 0x15

3,在CF位为0的情况下,“返回后调用”中对应的寄存器就会有结果。

BIOS中断0X15子功能0Xe801获取内存

AX=0XE801:分别检测低15MB和16MB-4GB的内存,最大支持4GB。

在15M空间容量=AX*1024,AX与CX最大值为0x3c00,0x3c00*1024=15M。

在16M-4GB的内存容量=BX*64*1024。

所以我们可以看出来要在我们求出的容量上面加上1MB。

步骤为:

1,将AX寄存器写入0xE801

2,执行中断调用int 0x15

3,在CF位位0的情况下,“返回后输出”中对应的寄存器便有相应结果。

BIOS中断0X88获取内存

AH=0X88:最多检测出64MB内存,实际内存超过64MB按照64MB返回。

求出来也要在加上1MB。

步骤为:

1,将AX寄存器写入0X88

2,执行中断调用int 0X15

3,在CF位为0的情况下,“返回后输出”中对应的寄存器就是结果。


;equ属于伪指令,是个替换指令,只是告诉编译器我们定义的这个名字相当于后面的东西,但并不对内存进行操作,只对编译器进行操作。

;我们现在生成的文件都是存bin文件,所以需要注意的是代码段和数据段不同属性都是放在一个段中的,而不是和elf格式一样将相同属性的段放在一起,里面也没有elf格式的地址信息,所以我们一定要将定义的数据和可执行的代码分开,数据段放在一起,代码段放在一起,然后jmp或者call时候加上标号就行了,这样就不会执行到数据段出错了。

;我们现在是实模式下面,默认的16位,所以会编译器会根据32位资源生成相应的前缀反转机器码,到进入保护模式时候一定加上[bits 32]

total_mem_bytes dd 0
ards_nr dw 0; 用于记录ards结构体数量
loader_start :
; ---- int 15h eax = 0000E820h, edx = 534D4150h获取内存布局,用edx来存放结果----
xor ebx, ebx; 第一次调用时,ebx值要为0,现在处于实模式下面,默认[bits 16]编译器会生成相应的机器码加上前缀反转
mov edx, 0x534d4150; edx只赋值一次,循环体中不会改变
mov di, ards_buf; ards结构缓冲区
mov ecx, 20; ARDS地址范围描述符结构大小是20字节
.e820_mem_get_loop:; 循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820; 执行int 0x15后, eax值变为0x534d4150, 所以每次执行int前都要更新为子功能号。
int 0x15
jc.e820_failed_so_try_e801; 此时函数执行完毕,先判断cf位,若cf位为1则有错误发生,尝试0xe801子功能
add di, cx; 若cf不等于1,则使di增加20字节指向缓冲区中新的ARDS结构位置
inc word[ards_nr]; 记录ARDS数量
cmp ebx, 0; cmp相当于减法,将ebx和0比较,若ebx为0且cf不为1, 这说明ards全部返回,当前已是最后一个
jnz.e820_mem_get_loop; 若ebx不等于0则继续循环,若等于0则往下面执行; 在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr]; 遍历每一个ARDS结构体, 循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx; edx为最大的内存容量, 在此先清0.find_max_mem_area:; 无须判断type是否为1, 最大的内存块一定是DRAM,可以被使用的
mov eax, [ebx]; base_add_low
add eax, [ebx + 8]; length_low
add ebx, 20; 指向缓冲区中下一个ARDS结构
cmp edx, eax; 冒泡排序,找出最大, edx寄存器始终是最大的内存容量
jge.next_ards; edx - eax,若edx大于等于eax时候jump过去,表示edx中的值不改变还是最大值
mov edx, eax; 否则赋值,edx为总内存大小
.next_ards:; jump族后面加的是标号,不能跟指令,所以不能jge后面加上loop。。
loop.find_max_mem_area;此时循环体执行完毕,将cx减一,然后判断循环条件,cx与0比较,若等于0,则跳出,不等于0执行循环
jmp.mem_get_ok; ------  int 15h ax = E801h 获取内存大小, 最大支持4G,所以用32位寄存器edx来存放结果------
; 返回后, ax cx 值一样, 以KB为单位, bx dx值一样, 以64KB为单位
; 在ax和cx寄存器中为低16M, 在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax, 0xe801
int 0x15
jc.e801_failed_so_try88; 函数执行完毕,判断先判断cf位,若当前e801方法失败, 就尝试0x88方法; 1 先算出低15M的内存, ax和cx中是以KB为单位的内存数量, 将其转换为以byte为单位
mov cx, 0x400; cx和ax值一样, cx用做乘数,0x400的十进制是1024
mul cx; cx为16为,另一个乘数是ax,ax*cx = dx:ax
shl edx, 16; edx左移16位,将低16位移到高16位
and eax, 0x0000FFFF; 然后将eax的低16保持原值,高16位都置0
or edx, eax; 相加,所以本质就是将dx:ax中的值移动到edx中
add edx, 0x100000; ax只是15MB, 故要加1MB
mov esi, edx; 先把低15MB的内存容量存入esi寄存器备份; 2 再将16MB以上的内存转换为byte为单位, 寄存器bx和dx中是以64KB为单位的内存数量
xor eax, eax
mov ax, bx
mov ecx, 0x10000; 0x10000十进制为64KB
mul ecx; 32位乘法, 默认的被乘数是eax, 积为64位, 高32位存入edx, 低32位存入eax.edx:eax
add esi, eax; 由于此方法只能测出4G以内的内存, 故32位eax足够了, edx肯定为0, 只加eax便可
mov edx, esi; edx为总内存大小
jmp.mem_get_ok; --- - int 15h ah = 0x88 获取内存大小, 只能获取64M之内,所以要用32位寄存器edx存放结果-------
.e801_failed_so_try88:
; int 15后,ax存入的是以kb为单位的内存容量
mov  ah, 0x88
int  0x15
jc.error_hlt
and eax, 0x0000FFFF; 将低16位保持原值,高16位置0; 16位乘法,被乘数是ax, 积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400; 0x400等于1024, 将ax中的内存容量换为以byte为单位
mul cx	;cx*ax=dx:ax
shl edx, 16; 把dx移到高16位
or edx, eax; 把积的低16位组合到edx, 为32位的积
add edx, 0x100000; 0x88子功能只会返回1MB以上的内存, 故实际内存大小要加上1MB.mem_get_ok:
mov[total_mem_bytes], edx; 将内存换为byte单位后存入total_mem_bytes处。


开启保护模式

全局段描述符表GDT:

我们在实模式下面的内存访问形式为:段基址:段内偏移地址,保护模式要考虑到兼容问题,所以也才采用这种形式:段选择子:段偏移地址

各种描述符一般都是8个字节,64位。段寄存器叫做选择子-sector,会在段描述符中检查各种权限和寻找段基址,权限通过后才能访问硬件。

实模式下面地址宽度为20位,1M内存,保护模式下面是32位,大小位4G。

GDT中存放的都是全局段描述符,GDTR寄存器中存放着GDT的起始位置。GDTR位48位,0-15 这16位为GDT界限,GDT界限为GDT占的总长度字节,除以8字节得到GDT数量,最多定义2的16次方除以8=8192。16-47这32位为起始地址,格式为:

lgdt 48位内存数据


选择子结构:3-15位是索引值,2的13次方=8192,索引值表示的是第几个描述符,用GDT的起始地址+索引值*8=目标段描述符。第0个描述符不可用。TI和RPL专门将

打开A20地址线:

实模式下面是地址回绕的(wrap-around)。20地址线,1M内存,当地址进位到1MB以上时候,因为没有第21根地址线,会丢失进位,变成内存最低值。保护模式要打开A20这根地址线:将端口0x92第1位置1即可。

int al,0x92
or al,0000_0010B
out 0x92,al

打开保护模式的开关:CR0寄存器的PE位

PE位:protection enable,只要打开了这个开关,那么不管GDT和A20有没有设置好,都会进入保护模式,所以这是最后一步。

mov eax,cr0
or eax,0x00000001
mov cr0,eax


至此进入保护模式!注意的是进入保护模式后要设置[bits 32]和刷新流水线。

所以进入保护模式的三个步骤为:

1,设置好GDT和GDTR

2,打开A19地址线

3,打开cr0的PE位


开启分页模式

因为我们的系统设置的内存为32M,实模式下面我们使用的是分段机制,地址都是真实的物理地址,当CPU将段基址+段内偏移地址时候只能访问到32M。我们想实现和linux下面一样的内存机制:4G内存,操作系统运行在3G-4G虚拟地址之上,其他用户进程运行在3G虚拟地址之下。就是说我们的内存虽然只要32M,但是我们可以用虚拟地址来实现4G内存。不过本质上4G内存经过还原后还是在这32M内存中访问。

我们采用二级页表来构建分页机制。页是地址空间的计量单位,标准页的尺寸为4KB,那么4GB内存,就需要1MB个标准页。在一级页表中,只有一个页表,里面包含了1MB个页表项,每个页表项4字节大小,页表共4MB大小,一个页表项指向一个4KB大小的标准页,所以表示的范围为1MB*4KB=4G。在二级页表中,有1个页目录和1024个页表,页目录中有1024个页目录项,一个页目录项的大小为4B,页目录大小为4K(标准页大小)。一个页目录项指向一个页表,一个页表里有1024个页表项,一个页表大小就为4KB(标准页),一个页表项可以指向一个标准页,所以一个页表项表示的范围就是4M,所以一个页目录项表示4M大小内存。当然我们用不了这么多的页表,我们用多少分配多少就行。我们写的kernel只用不到1M内存大小,所以我们就只用第一个页表里面的256个页表项即可。

页目录项和页表项都是4字节大小,但是里面不全部都是地址值,只表示了31-12位这30位,低12位没有表示,默认为0。因为地址空间的单位都是标准页4K=0x1000。所以低12位都位0。比如一个PDE指向的是页表,一个页表是4K大小(1024项PTE),所以PDE中地址的前12位可以为0。PTE指向的是一个标准物理页,也是4K大小,所以PTE中地址的前12位也可以为0。


可以看到:有一个页目录表和1024个页表,但是我们的操作系统太小了用不到这么多,我们只用第一个页表的256项,表示出1M内存即可。

段部件:cpu集成了这个模块,当CPU要访问内存时候,要通过段寄存器和偏移地址访问,段部件集成了这个算法:首先先根据段选择子计算GDT中的段基址,然后加上偏移地址得到线性地址,这是个虚拟地址。

页部件:cpu集成了这个模块,当段部件输出线性地址后进入页部件,页部件会根据二级页表的情况来取得实际的物理地址。

具体方法为:

1,用虚拟地址的高10位乘以4,作为页目录表内的偏移地址,加上页目录表的起始物理地址,求和便是页目录项PDE的物理地址。读取该页目录项的值,便是所指页表的起始物理地址。//高10位,2的10次方=1024,页目录项有1024个,所以高10位的值就对应着 PDEn,乘以4加上起始地址为 PDEn 的具体物理地址。

2,用虚拟地址的中间10位乘以4,作为页表内的偏移地址,加上上一步得到的页表物理地址,求和便是页表项PTE的物理地址。读取该页表项的值,便是所指的物理页的起始物理地址。  //中间10位,2的10次方=1024,页表项有1024个,所以中间10位的值对应着 PTEn,乘以4加上起始地址为 PTEn 的具体物理地址。

3,用虚拟地址的低12位,作为物理页的偏移量,加上上一步所求的物理页的起始地址,得到我们想要访问的内存。


确定页目录和页表的物理地址,明确虚拟地址和实际地址的映射关系

页目录和页表的物理地址:  我们把页目录项的起始地址设在1M之上:0x100000,范围是0x100000-0x101000。我们只用一个页表项足以应付我们的1M的kenel,我们将页表项紧挨着页目录项:0x101000-0x102000。我们只用到这个页表项中的PTE0-PTE255这256项,一项指的范围为4KB,所以256项为1MB。

映射关系:  我们的内核要加载到32M内存的低1M之内,要想把我们的kernel运行到3G之上,就要用虚拟地址从3G起始的1M地址:(0xc000 0000--0xc010 0000)映射到我们的32M内存的低1M(0x0000 0000--0x0010 0000)。

根据映射关系填好相应的页目录项和页表项

当CPU拿到虚拟地址为3G=0xc000 0000应该访问物理地址0x0000 0000。按照虚拟地址的解析步骤,首先应该看的高10位是:高16位:1100 0000 0000 0000b,高10位:1100 0000 00-->0011 0000 0000-->0x300=768,所以要首先访问 PDE768 ,物理地址为:0x300*4+0x100000=0x100c00。将这个地址处的值填为PTE0,即0x101000。表示这一页目录项指向的是物理地址0x101000起始的页表。

接着看中间10项:0xc000 0000的中间10项为0,由于我们需要 PTE0--PTE255,一个页表项对应物理地址的4KB,所以PTE0 的值为0,访问到了物理地址0x0000 0000-0x0000 1000。最后虚拟地址的最后12位对应着的这4KB的具体每个字节。  同理,PTE1 对应着物理地址的 (0x0000 1000-0x0000 2000),所以PTE1的值位0x0000 1000。依次类推,最后的 PTE 255的值对应着 0x000E 0000-0x0010 0000,PTE255应该填入0x000F 0000。

这样便建立起来了我们内核的映射关系: 0xC000 0000--0xC010 0000 到 0x0000 0000--0x0010 0000。分页模式开启后,程序中的每个地址都是虚拟地址了,都要通过段部件和页部件来访问硬件。当kernel程序中要通过段选择子和偏移地址访问内存时候,CPU取到段寄存器和偏移地址值,首先经过段部件输出线性地址(虚拟地址),然后通过页部件来输出真实物理地址。

分页模式的步骤:

1,准备好页目录和页表

2,将页表地址写入CR3

3,将寄存器CR0的PG位置1


加载内核

之前我们编译的都是纯二进制程序,只含有机器码,没有其他地址信息,要靠我们手动的把机器码加载到指定内存中,所以我们的把MBR dd在第0个扇区(LBA),将loader dd在第2个扇区,然后在MBR的程序将loader从硬盘中加载到0x900内存处。我们用GCC编译器编译出来的程序默认被加载到0x0804 8000地址处左右。

GCC将一个程序生成可执行文件时,要经过预处理器,汇编器,编译器,链接器4步才能生成可执行文件。那么我们就可以使用链接器ld来指定可执行代码的虚拟地址,用-Ttext参数来指定,则CPU就会在此处开始执行程序。eg:ld main.o -Ttext 0xc000 1500 -e main -o kernel.bin

一个可执行的ELF文件有很多机器码是多余的,CPU执行了就会报错,一个纯二进制文件也不是说程序从开头的机器码就可以直接执行,因为可能是数据(我们的loader)。所以一个程序总该有入口地址,这个地址表示的是程序将从哪里开始执行。对于一个ELF可执行文件来说,它的地址是在链接阶段将几个ELF重定位文件链接在一起的,所以链接器规定只把名为_start的函数作为程序的入口地址。我们可以用-e来指定起始的函数名为main或者其他标号。若程序中没有_start,编译器将会生成一个_start,然后调用main函数来执行,所以没有_start时候,0xc000 1500处是_start,然后在往上面才是main。有了-e main 后,0xc000 1500就是main了,而不是_start。

ELF文件介绍

ELF文件包括:待重定位的ELF文件,可执行的ELF文件和共享目标文件。

程序中最重要的部分就是段(segment)和节(section),段是由节来组成的,多个相同属性的节链接在一起形成了段。

段和节的信息是由header来描述的,描述一个段的有关信息(大小、属性、偏移地址等)的叫做程序头(program header)(也叫段头,段就等同于程序),描述一个节的有关信息(大小、属性、偏移地址等)的叫做节头(section header)。由于段有很多,所以程序头就储存在程序头表(program header table)中,由于节也有很多,所以节头就储存在节头表(section header table)中。程序头表的大小和位置也是随着段的变化而变化的,节头表也是。所以我们用一个固定位置、固定大小的 ELF header来描述程序头表和节头表的大小和位置信息。ELF header位于文件最开始部分。

ELF文件(待重定位的ELF文件和可执行的ELF文件)分为文件头和文件体两部分。先用ELF header来简单的说了程序头表和节头表的大小和位置。然后根据这些找到程序头表和节头表,在具体的描述这些段和节的详细信息。ELF格式的作用体现在链接阶段和运行阶段。在待重定位文件中,文件开头必须是ELF header,下面可能是program header table,对于可执行的文件来说,文件开头必须是ELF header,下面必须是program header table。下图分别是待重定位文件和可执行文件的信息。

关于ELF的任何定义,都可以在linux系统的usr/include/elf.h中找到。

ELF header结构

e_ident[16]是16字节大小的的数组,


e_type 2字节,用来指定elf目标文件的类型


e_machine 2字节,描述elf目标文件在哪个平台上面运行
e_vertion 4字节,描述版本信息
e_entry 4字节,描述操作系统运行该程序时,将控制权转交到的虚拟地址(_start 入口地址)
e_phoff 4字节,指明程序头表在文件内的字节偏移量。若没有程序头表,则为0
e_shoff 4字节,指明节头表在文件内的字节偏移量,若没有节头表,则为0
e_flags 4字节,指明于处理器相关标志
e_ehsize  2字节,指明elf header的字节大小
e_phentsize  2字节,指明程序头表中每个条目的字节大小。即一个程序头只能描述一个段
e_phnum   2字节,指明程序头表中条目数量,即文件中段的数量(一个程序头只能描述一个段)
e_shentsize  2字节,指明节头表中每个条目的字节大小。即每个用来描述节信息的节头的大小
e_shnum 2字节,指明节头表中条目的数量,即节的个数
e_shstrndx 2字节,指明string name table 在节头表中的索引 index


程序头(program header)结构

一个程序头只能来描述一个段(segment)。
p_type 4字节,指明程序中该段的类型

p_offset 4字节,指明本段在文件中的起始偏移字节
p_vaddr 4字节,指明本段在内存中的起始虚拟地址
p_paddr 4字节,未设定
p_filesz 4字节,指明本段在文件中的大小
p_memsz 4字节,指明本段在内存中大小,和上面的相等
p_flags 4字节,相关标志


p_align 4字节,指明本段文件和内存中的对齐方式,为0/1表示不对齐,否则应是2的幂次数

******第一个段为.text段,本段包含了elf header、program header table和可执行机器码。所以不要认为.text段只包含可执行机器码。所以程序的起始虚拟地址指的是第一段即elf header 的开头地址,程序的入口地址值得是可执行代码_start或者-e main 的地址。

将内核载入内存要有两个步骤

1,加载内核到内存缓冲区:需要把内核文件加载到内存缓冲区,只是将elf内核从硬盘上拷贝到内存中。

2,将内核从内存缓冲区加载到特定的虚拟内存处。通过分析elf header和program header table的结构只将segment加载到特定的虚拟内存处。

所以内核在内存中有两份拷贝,一份是ELF格式的原文件kernel,另一份是loader经过解析后的内核映像。过程为:要解析elf header 的e_phentsize找到program header的大小,然后找到e_phoff找到program header table在文件中的偏移量,然后找到e_phnum,表示有几个program header。由于第一个段.text也包括了elf header 和 program header table 和可执行机器代码,所以我们加载到指定的虚拟内存时候其实也是把各种header也加载到了里面。

大数据复制三剑客:cld、(ecx、esi、edi、),rep  movs[bwd]

kernel_init:
xor eax,eax
xor ebx,ebx	;ebx记录程序头表的起始地址
xor ecx,ecx	;ecx记录程序头表中程序头的数量
xor edx,edx	;edx记录程序头表中一个程序头的大小mov dx,[KERNEL_BIN_BASE_ADDR+42]	;偏移42字节处是e_phentsize,表示程序头大小
mov ebx,[KERNEL_BIN_BASE_ADDR+28]	;偏移28字节是e_phoff,第一个程序头的偏移地址add ebx,KERNEL_BIN_BASE_ADDR		;ebx为第一个段的实际地址
mov cx,[KERNEL_BIN_BASE_ADDR+44]	;偏移44字节是e_phnum,表示有几个程序头 .loop_each_segment:		;每一个段都调用一次 内存复制 函数
cmp byte[ebx+0],PT_NULL		;第一个段的类型是PT_NULL,则终止
je .PTNULLpush dword[ebx+16] 		;先压入程序头的p_filesz,表示一个段的大小
mov eax,[ebx+4]			;程序头的p_offset,表示一个段的的偏移地址:即起始地址
add eax,KERNEL_BIN_BASE_ADDR	;eax中表示这个段的起始地址:即段大小
push ax
push dword[ebx+8]		;表示一个段的目的虚拟地址:即目的地址call mem_cpy
add esp+12.PTNULL:
add ebx,edx  		;指向指向下一个段
loop .loop_each_segmentmem_cpy:
cld
push ebp
push ecx
mov ebx,esp
mov edi,[ebp+8]
mov esi,[ebp+12]
mov ecx,[ebp+16]
rep movsb		;一次复制一字节,复制一次ecx减1,esi和sdi自动加一字节,然后将ecx和0比较,不相等则继续赋值pop ecx
pop ebp
ret




这篇关于二、进入保护模式--内核加载器LOADER:实模式下内存容量检测、开启保护模式、开启分页模式、加载kernel到内存缓冲区、加载kernel到内存(内存复制函数)-kernel的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/841216

相关文章

Linux内核之内核裁剪详解

《Linux内核之内核裁剪详解》Linux内核裁剪是通过移除不必要的功能和模块,调整配置参数来优化内核,以满足特定需求,裁剪的方法包括使用配置选项、模块化设计和优化配置参数,图形裁剪工具如makeme... 目录简介一、 裁剪的原因二、裁剪的方法三、图形裁剪工具四、操作说明五、make menuconfig

关于Java内存访问重排序的研究

《关于Java内存访问重排序的研究》文章主要介绍了重排序现象及其在多线程编程中的影响,包括内存可见性问题和Java内存模型中对重排序的规则... 目录什么是重排序重排序图解重排序实验as-if-serial语义内存访问重排序与内存可见性内存访问重排序与Java内存模型重排序示意表内存屏障内存屏障示意表Int

最好用的WPF加载动画功能

《最好用的WPF加载动画功能》当开发应用程序时,提供良好的用户体验(UX)是至关重要的,加载动画作为一种有效的沟通工具,它不仅能告知用户系统正在工作,还能够通过视觉上的吸引力来增强整体用户体验,本文给... 目录前言需求分析高级用法综合案例总结最后前言当开发应用程序时,提供良好的用户体验(UX)是至关重要

如何测试计算机的内存是否存在问题? 判断电脑内存故障的多种方法

《如何测试计算机的内存是否存在问题?判断电脑内存故障的多种方法》内存是电脑中非常重要的组件之一,如果内存出现故障,可能会导致电脑出现各种问题,如蓝屏、死机、程序崩溃等,如何判断内存是否出现故障呢?下... 如果你的电脑是崩溃、冻结还是不稳定,那么它的内存可能有问题。要进行检查,你可以使用Windows 11

如何安装HWE内核? Ubuntu安装hwe内核解决硬件太新的问题

《如何安装HWE内核?Ubuntu安装hwe内核解决硬件太新的问题》今天的主角就是hwe内核(hardwareenablementkernel),一般安装的Ubuntu都是初始内核,不能很好地支... 对于追求系统稳定性,又想充分利用最新硬件特性的 Ubuntu 用户来说,HWEXBQgUbdlna(Har

MyBatis延迟加载的处理方案

《MyBatis延迟加载的处理方案》MyBatis支持延迟加载(LazyLoading),允许在需要数据时才从数据库加载,而不是在查询结果第一次返回时就立即加载所有数据,延迟加载的核心思想是,将关联对... 目录MyBATis如何处理延迟加载?延迟加载的原理1. 开启延迟加载2. 延迟加载的配置2.1 使用

Android WebView的加载超时处理方案

《AndroidWebView的加载超时处理方案》在Android开发中,WebView是一个常用的组件,用于在应用中嵌入网页,然而,当网络状况不佳或页面加载过慢时,用户可能会遇到加载超时的问题,本... 目录引言一、WebView加载超时的原因二、加载超时处理方案1. 使用Handler和Timer进行超

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

hadoop开启回收站配置

开启回收站功能,可以将删除的文件在不超时的情况下,恢复原数据,起到防止误删除、备份等作用。 开启回收站功能参数说明 (1)默认值fs.trash.interval = 0,0表示禁用回收站;其他值表示设置文件的存活时间。 (2)默认值fs.trash.checkpoint.interval = 0,检查回收站的间隔时间。如果该值为0,则该值设置和fs.trash.interval的参数值相等。