zoukankan      html  css  js  c++  java
  • 操作系统开发系列—12.c.从Loader加载ELF内核,顺便解释下函数调用过程 ●

    实际上,我们要做的工作是根据内核的Program header table的信息进行类似下面这个C语言语句的内存复制:

    memcpy(p_vaddr, BaseOfLoaderPhyAddr+p_offset, p_filesz);

    复制可能不止一次,如果Program header有n个,复制就进行n次。

    每一个Program header都描述一个段,语句中的P_offset为段在文件中的偏移,p_filesz为段在文件中的长度,p_vaddr为段在内存中的虚拟地址。

    由ld生成的可执行文件中p_vaddr的值总是一个类似于0x8048XXX的值,至少我们的例子中是一个这样的值。可是我们启动分页机制时地址都是对等映射的,内存地址0x8048XXX已经处在128MB内存以外(128MB的十六进制表示是0x8000000),如果计算机的内存小于128MB的话,这个地址显然已经超出了内存大小。

    即便计算机有足够大的内存,显然,我们也不能让编译器来决定内核加载到什么地方。解决它有两个办法,一是通过修改页表让0x8048XXX映射到较低的地址,另一种方法就是通过修改ld的选项让它生成的可执行代码中p_vaddr的值变小。

    nasm -f elf -o kernel.o kernel.asm

    ld -m elf_i386 -s -Ttext 0x30400 -o kernel.bin kernel.o

    程序的入口地址就变成0x30400了,ELF header等信息会位于0x30400之前。此时的ELF header和Program header table的情况如下表所示:

    根据上表,我们应该这样放置内核:

    memcpy(30000h, 90000h+0, 40Dh);

    也就是说,我们应该把文件从开头开始40Dh字节的内容放到内存30000h处。由于程序的入口在30400h处,所以从这里就可以看出,实际上代码只有0Dh+1个字节。下面是Kernel.bin的内容:

    上面被星号省去的部分都是0.从中可以看出,从400h到40Dh是仅有的代码,0xEBFE正是代码最后的“jmp $”。

    下面的代码实现了将Kernel.bin根据ELF文件信息转移到正确的位置。它很简单,找出每个Program header,根据其信息进行内存复制:

    ; 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    

    InitKernel源码解析:

    ELF文件首先是ELF header,首先要获取的e_phnum值离文件开头有1*16+2*2+4+4+4+4+4+2*2=44=2C字节,e_phnum本身是2个字节;e_phoff离文件开头有1*16+2*2+4+4=28=1C字节,e_phoff值本身是4个字节,它代表的是Program header table在文件中的偏移量(以字节计数),所以在.Begin之前esi最终指向的是ELF文件中的Program header table的开头。

    MemCpy源码解析:

    16位CPU入栈和出栈操作都是以字(16位)为单位进行的。

    32位CPU入栈和出栈操作是以32位为单位进行的。

    现在esi是从Program header里找,p_filesz离Program header开头有4+4+2*4=16=10h字节,p_filesz本身是4个字节,把它最先压入栈,push  dword [esi + 010h];紧接着是压入p_offset(段的第一个字节在文件中的偏移)的值对应的那个位置;最后压入的是p_vaddr的值,然后调用MemCpy函数,栈内的内容如下所示:

    现在来看MemCpy的代码:

    ; ------------------------------------------------------------------------
    ; 内存拷贝,仿 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 结束------------------------------
    

    调用call指令的时候会自动把eip压入栈中,MemCpy第一句把ebp压栈,所以现在栈内情况如下图:

    然后把esp的值赋给ebp,因此[ebp + 8]的值就是dst,[ebp+12]就是src,[ebp+16]就是栈最底部的size。然后就是逐字节把src的内容复制到dst,最后把dst的值当成返回值赋给eax寄存器。最后依次弹出压入的值并调用ret(pop eip),那么栈里还剩下dst、src、size共12字节的数据,所以在call MemCpy之后是add esp, 12.

    接下来就是向内核跳转

    ;***************************************************************
    	jmp	SelectorFlatC:KernelEntryPointPhyAddr	; 正式进入内核 *
    	;***************************************************************
    

    KernelEntryPointPhyAddr定义在头文件load.inc中,其值为0x30400.当然,它必须跟我们的ld的参数-Ttext指定的值是一致的。将来如果我们想将内核放在另外的位置,只需改动这两个地方就可以了。

    运行结果如下:

    成功了,出现字符“K”,这表明我们的内核在执行了。Loader的使命圆满结束。

    一个码农的日常 

    源码

  • 相关阅读:
    【转】MyEclipse快捷键大全
    【转】MOCK测试
    【转】万亿移动支付产业的难点和痛点
    【转】【CTO俱乐部走进支付宝】探索支付宝背后的那些技术 部分
    CTO俱乐部
    tomcat修改默认端口
    VS2013试用期结束后如何激活
    项目中遇到的 linq datatable select
    LINQ系列:LINQ to DataSet的DataTable操作
    C#中毫米与像素的换算方法
  • 原文地址:https://www.cnblogs.com/joey-hua/p/5401760.html
Copyright © 2011-2022 走看看