Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> Linux內核系列—12.c.操作系統開發之從Loader加載ELF內核,順便解釋下函數調用過程 ●,12.celf

Linux內核系列—12.c.操作系統開發之從Loader加載ELF內核,順便解釋下函數調用過程 ●,12.celf

編輯:關於android開發

Linux內核系列—12.c.操作系統開發之從Loader加載ELF內核,順便解釋下函數調用過程 ●,12.celf


實際上,我們要做的工作是根據內核的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源碼解析:

現在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的使命圓滿結束。

 

一個碼農的日常 

源碼

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved