Chinaunix首页 | 论坛 | 博客
  • 博客访问: 484855
  • 博文数量: 164
  • 博客积分: 4024
  • 博客等级: 上校
  • 技术积分: 1580
  • 用 户 组: 普通用户
  • 注册时间: 2009-10-10 16:27
文章分类

全部博文(164)

文章存档

2011年(1)

2010年(108)

2009年(55)

我的朋友

分类:

2010-06-28 13:51:39

【用C语言编写内核程序】

一、读入ELF文件到内存

接着Loader.bin 程序将,在进行保护模式的系列初始化工作后。是时候转到kernel.bin了。

1、首先将 Kernel.bin 装入到内存:

此时的Kernel,bin文件里面的内容跟磁盘上存放的是一摸一样。因为这个时候的装载是直接N个扇区到内存。;

2、分析内存中的Kernel.bin文件:

因为在内存当中的Kernel.bin内容,与ELF结构是一模一样的。那么现在就可以按照的ELF header 有用的信息进行内存分布了。而需要分布到内存的数据就是Program header 项,有n Program header项就要分别N个段到内存。并且根据ELF Header 以及Program Header 表述的信息进行分布。

在linux 平台用 ld 链接命令链接程序的时候。总是将程序的p_vaddr默认分布到0x848××××地址。0x848××××已经超过了128MB了。这个在linux操作系统下,是线性分页地址。linux 自己会转换。可是我们现在自己的操作系统还没转换这个线性地址对应页表项,。所以可以在ld链接的时候指定低一点的地址(ld -s -Ttext 0x30400 -o kernel.bin kernel.o)。 这个0x30400地址指定的是程序代码入口的地址。那么ELF_header的信息以及Program header表信息也就对应在0x30400的前面。

之所以要把入口定义到(e_entry) 0x30400位置.是为了便于调式。500h -9fbffh是我们核心代码可以用到得。 那么当我们使用ld链接成0x30400位置的时候kernel.bin 的字段会自动被设置。 p_fielSz==p_memSz== 40dh(段在文件中的长度,), p_vaddr的值是30000h它表示 程序需要被装载的偏移为30000h的内存虚拟地址。

那么MemCpy(30000h,90000h+0,40dh) 表示将kernel.bin从开始移动40dh个字节到30000h。那么3040dh就是程序的末尾了,不过入口地址是在30400h.那么实际代码就是 30400h-3040dh=(d+1个字节)。

*下面是分析一个ELF文件并且将它以代码段入口为0x30400为标准,把所有的段都装载低地址。

;------------找出ELF文件信息------------------
InitKernel:
   xor esi,esi
  
   mov cx,WORD [BaseOfKernelFilePhyAddr + 2ch] ;Kernel.bin被加载到得物理地址+ 2ch e_phnum字段的值
   movzx ecx,cx             ;Program header的个数,就是ELF程序共多少个段,这里是1
  
   mov esi,[BaseOfKernelFilePhyAddr + 1ch];e__phOff   ;Program header table 的偏移位置
   add esi,BaseOfKernelFilePhyAddr   ;得到第一个Program header
  
.begin:
   mov eax,[esi + 0]
   cmp eax,0     ;如果p_type为0表示错误不是正常段
   jz   .NoAction
  
   push DWORD [esi + 010h]   ;MemCpy第三个参数 Copy size
   mov eax,[esi + 4h]     ;p_offset 段在文件中第一个字节的偏移值
   add eax,BaseOfKernelFilePhyAddr
   push eax        ;MemCpy第二个参数数据源基地址,
  
   push DWORD [esi + 08h]       ;MemCpy第一个参数目标基地址,
  
   call MemCpy
   add esp,12
.NoAction:
   add esi,020h     ;多个段的时候寻找完所有段 Program header 大小20h
   dec ecx
   jnz .begin
   retn

接下来我们可以 :

call InitKernel

jmp   SelectorFlatC:KernelEntryPointPhyAddr ;直接进入了内核。

到此以后可以用C语言进行编程了。

二、Kernel走向:

×千辛万苦终于走到了Kernel.bin 里面来了。kernel.asm的内容如下:

;=======================kernel.asm===============================;

SELECTOR_KERNEL_CS        equ 8      ;这个8也就是段选择子的属性 ,8也就是第一个GDT描述符RPL=0 TIL=0,

;外部导入函数以及全局变量

extern    cstart        ;第一个C内核函数

extern gdt_ptr         ;gdt_ptr GDT描述符指针,6个字节共48位,16位表示GDT界限。32表示基地址.

[section .bss]         ;.bss表示已访问,也就是说不初始化。内存中的数据是无可预料的。

StackSpace resb    2 * 1024 ;这个resb是个伪指令 表示定义未初始的字节,

StackTop :             ;保护模式下的栈顶 偏移值

[section .text]

global _start

_start:                 ;这里就会被ld 链接到KernelEntryPointPhyAddr位置

         mov esp,StackTop         ;将C语言下的新堆栈地给esp

         sgdt     [gdt_ptr]          ;保存GDTR的内容到gdt_ptr里面 .

        call        cstart          ;已经将 GDTR传给C语言代码区

          lgdt [gdt_ptr]      ;经过C代码的处理加载新的GDT到GDTR

jmp              SELECTOR_KERNEL_CS:csinit

csinit:

      hlt         ;等待中断 以上的寻址都是分页寻址。只不过分页的地址于物理地址暂时是对应的。

×接下来让我们来看看C代码区是怎么样处理传进去的 gdt_ptr.

C语言比不可少的头文件。优化代码的可阅读性啊。先把常用的常量、数据类型、以及保护模式需要的结构体头文件列出来。

;---------------------type.h-----------------------------

//这里定义一些基本数据类型.

#ifndef _TYPE_H_

#define _TYPE_H_              //告诉编译器此处已经定义了_TYPE_H_头文件,不需重复定义了

typedef    unsigned int     t_32; //自定义无符号int

typedef   unsigned short t_16;//自定义无符号short

tepydef   unsigned char t_8 ;//自定义无符号char 无符号也就是全位可用。都是正

#endif

;---------------------------const.h-------------------------------------

//这里定义常量,以及宏...

#ifndef _CONST_H_

#define _CONST_H_

#define PUBLIC                  ;这些宏没有实际意义只是增加代码的可阅读,能显示的表达变量的作用

#define PRIVATE static      ;局部静态变量, C 有static关键字表示静态局部变量.

#define GDT_SIZE 128    ;GDT的最大长度

#endif

;---------------------protect.h-----------------------

//这里定义保护模式需要的结构体

#ifndef _PROTECT_H_

#define _PROTECT_H_

typedef struct          //与Descriptor宏类似,

{

              t_16 limit_low      ;段界限的低16 (共20位)。

              t_16 base_low ;段基址低16(共32位)

             t_8 base_mid ;段基址中间8位

               t_8 attr1;        段属性低8位(共12位)

               t_8 limit_high_attr2;     段界限的高4位加上段属性的高4位

                t_8_base_hight                 ;段基址高8位

}DESCRIPTOR

#endif

;--------------------以上都是些数据类型与常量的定义------------

下面这个就是功能模块函数块.

;--------------------string.asm----------------

//memcpy函数模块.功能就是将堆栈参数 以二参数为源 传三参数个字节数据到 一参数目标地址

[section.text]

global memcpy

memcpy:

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    ; 函数结束,返回

;===================start.c==========================

//这个就是Kernel 跳进来后的第一个C代码内核了

PUBLIC void* memcpy(void* pDst ,void* pSrc,int iSize); 显示的说明这个是全局函数,返回值空. 参数也就是一个32位的数据指针,具体无类型限制.

PUBLIC t_8          gdt_ptr[6]      //这个就是被kernel赋值的GDT结构体数组,

PUBLIC t_DESCRIPTOR        gdt[GDT_SIZE]; 定义128个元素的 DESCRIPTOR数组.

PUBLIC void cstart()            //有kernel汇编跳进来

{

    //第一个参数就是存放所有GDT描述符段的新地址,

   //第二个参数就是被Kernel 写进的GDTR内容,它指向GDT的基地址与界限,它的语法有点繁杂也不难理 。最左边那个*是取值, 而t_32*是类型的说明,它把&gdt_ptr[2]的值改成是一个指向32位int类型地址, 那么最左边的那个*取出的值将是一个 t_32类型的值,也就是32位的地址值。

//第三个参数是GDT元素个数,这个跟t_32类似,最左边那个*是取值,t_16*把&gdt_ptr[0]的值改成是指向一个16位short类型的地址,那么最左边的*取出的就是16位short的值。加1不难理解,GDT界限只是代表当前段开始,

      memcpy(&gdt,(*((t_32*)(&gdt_ptr[2])),*((t_16*)(&gdt_ptr[0]))+1);

t_16* p_gdt_limit   = (t_16*)(&gdt_ptr[0]);

t_32* p_gdt_base=(t_32*)(&gdt_ptr[2]);         //这个是老的GDT结构体指针内容

p_gdt_limit = GDT_SIZE * sizeof (DESXRIPTOR); 定义属于程序的新的GDT指针结体.GDT界限

p_gdt_base = (t_32) &gdt ;将新的GDT基址的地址转换成32位,付给新的GDT指针结构体

//到这里已经GDT有新位置 了。这两个变量用做sgdt lgdt[gdt_ptr]的参数.保存.

}

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

nasm -f elf -o string.o string.asm

gcc -c -fno-builtin -o start.o start.c         ;start.c代码中用到memcpy 系统函数关键字。用fno-builtin忽略它

ld -s -Ttext 0x30400 -o kernel.bin kernel.o start.o string.o

那么kernel.bin 内核已经生成了。现在跳到Kernel.bin后会跳到C(cstart())代码去.

×热烈欢迎cstart()函数上任主控:

到此我们的代码文件以及头文件已经非常之多了,那么编译起来也十分不方便。于是乎我们必需要学习一下makefile 选择性编译宏.

下面将我们的这个nasm工程用makefile 来配置一下:

;---------------------------makefile---------------------------

ASM                    =   nasm   # 这个是宏定义,引用nasm 字符串

ASMFLAGS        =   -I ./include         #这个是nasm 一个参数而已,表示环境变量的目录

TARGET           = boot.bin loader.bin       #目标参数宏,结果宏,也就是最终的结果就是要这两个文件.

.PHONY            = everything clean all    # .PHONY表示这些项并不是文件 仅仅是一种行为结果标号!

everything:$(TARGET)         # everything是结果;必备条件是TARGET 存在 .

clean:                #想要clean结果 不需要必备条件

    rm -f $(TARGET) #但是如果引用到clean,则会自动触发这行命令,命令中也可有依赖关系

all: clean everything     #想要all结果,必备条件是clean 存在(触发命令)、everything 存在(根据依赖关系创建文件),

#:号左边表示目标结果,:右边表示必要条件。也就是如果想要得到:左边的结果。必需:右的文件要存在.

boot.bin:boot.asm ./include/load.inc ./include/fat12hdr.inc

# $@ 表示目标结果第一个项,$< 表示必备条件的第一个项

$(ASM) $(ASMFLAGS) -o $@ $<           # 这个对应与它上方的第一行。

loader.bin:loader.asm ./include/load.inc ./include/fat12hdr.inc

$(ASM) $(ASMFLAGS) -o $@ $<          # 这个对应与它上方的第一行。

好了来编译我们的项目..

make all ,如果不指定结果。那么默认会从第一个冒号结果出执行。

make 默认会找寻当前目录下的makefile文件.,这条命令是要求得到all的结果。那么make会根据all的必备条件判断,当前all的必备条件是 clean everything 、那么就会分明去实现clean的必备条件与everything的必备条件!。clean的必备条件没有。但是它触发一行指令。。 那么也就完成了clean的结果。。而everything的必备条件是TARGET那么就需要这两个文件的必备关系。接着执行了这两个文件的命令。最后成功得到 Boot.bin、Loader.bin。

×在下一集里要总结一下工程;;以及文件功能与关系。

阅读(1134) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~