将存储在磁盘中的程序读取到内存,并以进程(或者叫任务)的方式来运行程序,是内核必须具备的一项功能...

    v0.0.11的项目地址:

    github.com地址:https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)
   
    Dropbox地址:点此进入Dropbox网盘  (该版本位于zenglOX_v0.0.11的文件夹,该文件夹里的zip压缩包为源代码)

    sourceforge地址:https://sourceforge.net/projects/zenglox/files  (该版本位于zenglOX_v0.0.11的文件夹,该文件夹里的zip压缩包为源代码)

    有关zenglOX的编译及gdb调试的方法,请参考zenglOX v0.0.1里的文章。

    将存储在磁盘中的程序读取到内存,并以进程(或者叫任务)的方式来运行程序,是内核必须具备的一项功能。

    之前的v0.0.7的版本里,已经实现了ram disk(存储在内存中的虚拟磁盘),并且在内核里也提供了相应的接口,可以从ram disk中读取出所需的文件内容。另外,v0.0.8的版本实现了Multitask(多任务),内核运行程序所需的两个基本条件已经准备好了,只需要再确定好程序的可执行文件的格式即可,像windows系统下是PE(Portable Executable)格式,Linux系统下是ELF(Executable Linkable Format)格式,它们都是COFF(Common file format)格式的变种。

    在这些可执行文件格式里存储了程序代码段和数据段的相关信息,比如代码段在文件里的偏移值,以及代码段在虚拟内存中的线性地址,代码段的起始入口的线性地址等。

    由于我们是在Linux下搭建的交叉编译环境,所以v0.0.11版本所使用的可执行文件格式是ELF格式,所有可以运行的程序在ram disk里都是以ELF的可执行文件格式进行存储的,在加载运行程序时,只需通过VFS(虚拟文件系统)提供的接口先从ram disk里将程序的文件内容读取到缓存中,然后对缓存里的文件内容按照ELF格式进行分析,并通过分析的结果,将文件内容中的代码段和数据段里的数据读取到ELF格式指定的线性地址,最后再转到代码段的入口地址去执行即可,这里只是先做个简单的介绍,下面会对整个执行过程做详细分析。

    为了测试ELF可执行文件格式,该版本里创建了一个cpuid的程序,该程序的汇编源代码"cpuid.s"存储在build_initrd_img目录中:

#cpuid.s Sample program to extract the processor Vendor ID
.section .data
output:
    .asciz "The processor Vendor ID is 'xxxxxxxxxxxx'\n"
.section .text
.globl _start
_start:
    movl $0, %eax
    cpuid
    movl $output, %edi
    movl %ebx, 28(%edi)
    movl %edx, 32(%edi)
    movl %ecx, 36(%edi)
    movl $0, %eax
    movl $output, %ebx
    int $0x80
hlt:
    jmp hlt


    上面的cpuid.s程式修改自"汇编语言"栏目的"汇编开发示例 (一)"里的文章,有关cpuid指令的原理和其他汇编指令的作用,请参考这篇文章,这里主要需要进行说明的是,由于目前zenglOX的系统调用号与Linux的不同,所以在执行int $0x80之前,EAX里设置的系统调用号为0,该调用号对应的功能是将EBX指向的字符串给显示到屏幕上,整个cpuid.s程式的作用是读取当前处理器的供应商ID字符串信息,并将该信息显示到屏幕上。

    cpuid程式的创建过程可以查看build_initrd_img目录里的makefile文件:

#makefile for ram disk

CC = @gcc

CFLAGS = -std=gnu99 -ffreestanding -gdwarf-2 -g3 -Wall -Wextra
INITRD_FILES = test.txt test2.txt cpuid

initrd.img:make_initrd.c $(INITRD_FILES)
    @echo 'building initrd.img'
    $(CC) -o make_initrd make_initrd.c $(CFLAGS)
    ./make_initrd $(INITRD_FILES)

cpuid:cpuid.o
    @echo "building $@"
    $(CROSS_LD) -o $@ $<

cpuid.o: cpuid.s
    @echo "building $@"
    $(CROSS_AS) -o $@ $< $(CROSS_AS_FLAGS)

clean:
    $(RM) $(RMFLAGS) *.o
    $(RM) $(RMFLAGS) cpuid
    $(RM) $(RMFLAGS) make_initrd
    $(RM) $(RMFLAGS) initrd.img

all: initrd.img


    上面的CROSS_LD,CROSS_AS,CROSS_AS_FLAGS,RM以及RMFLAGS变量的值,都是从顶层目录的makefile文件里使用export关键字导出来的。

    其中,CROSS_LD的值为交叉编译的链接器,CROSS_AS的值为交叉编译的汇编器,CROSS_AS_FLAGS是汇编器生成目标文件时的一些选项参数(当前版本为-gstabs参数,表示生成的目标文件里将包含相关的调试信息),在CROSS_AS汇编器生成cpuid.s对应的cpuid.o的目标文件后,再通过CROSS_LD链接器就可以生成cpuid可执行文件了(该可执行文件使用的就是ELF的文件格式)。

    在创建完cpuid可执行文件后,最后就可以通过make_initrd程序将test.txt,test2.txt以及cpuid三个文件写入到initrd.img文件里(initrd.img在之前的版本中讲解过,该文件会在启动时,被grub启动器自动加载到内存中,作为zenglOX的ram disk)。

    要加载执行ELF格式的可执行文件,首先就必须了解ELF格式,在 http://wiki.osdev.org/ELF_Tutorial 该链接里就通过例子详细说明了ELF格式,下面也会根据当前版本的代码来进行说明。

    ELF可执行文件的整体结构如下图所示:


图1

    从上图中可以看到,ELF可执行文件由ELF header,program header table,section header table以及各种Segment组成(Segment可以位于section header table的前面,也可以位于section header table的后面)。

    ELF header位于文件的开头,通过ELF header里的相关数据可以分别找到program header table以及section header table的起始偏移值以及尺寸大小等信息,而各种Segment的起始偏移值及尺寸大小等信息,则可以通过section header table里的相关数据来找到,程序中实际的指令代码以及各种与程序相关的数据都存储在Segment里。

    在Linux里,可以通过man elf命令来查看到ELF格式的说明文档,在这些说明文档里介绍了和ELF各部分相关的结构体的定义,以及结构体中各个字段的含义,在介绍这些字段的有效值时,说明文档里用到了很多宏名,例如:ELFCLASS32之类的,这些宏对应的值,可以在/usr/include/elf.h头文件里搜索到。

ELF header

    v0.0.11版本中,和ELF相关的代码位于zlox_elf.c文件里,和ELF相关的结构体则定义在zlox_elf.h的头文件里。

    下面我们先来看下和ELF header相关的结构体定义:

/*zlox_elf.h 和ELF可执行文件格式相关的定义*/

#ifndef _ZLOX_ELF_H_
#define _ZLOX_ELF_H_

................................
................................

#define ZLOX_ELF_NIDENT    16

................................

typedef ZLOX_UINT16 ZLOX_ELF32_HALF;
typedef ZLOX_UINT32 ZLOX_ELF32_OFF;
typedef ZLOX_UINT32 ZLOX_ELF32_ADDR;
typedef ZLOX_UINT32 ZLOX_ELF32_WORD;
typedef ZLOX_SINT32 ZLOX_ELF32_SWORD;

................................
................................

typedef struct _ZLOX_ELF32_EHDR{
	ZLOX_UINT8	e_ident[ZLOX_ELF_NIDENT];
	ZLOX_ELF32_HALF	e_type;
	ZLOX_ELF32_HALF	e_machine;
	ZLOX_ELF32_WORD	e_version;
	ZLOX_ELF32_ADDR	e_entry;
	ZLOX_ELF32_OFF	e_phoff;
	ZLOX_ELF32_OFF	e_shoff;
	ZLOX_ELF32_WORD	e_flags;
	ZLOX_ELF32_HALF	e_ehsize;
	ZLOX_ELF32_HALF	e_phentsize;
	ZLOX_ELF32_HALF	e_phnum;
	ZLOX_ELF32_HALF	e_shentsize;
	ZLOX_ELF32_HALF	e_shnum;
	ZLOX_ELF32_HALF	e_shstrndx;
} ZLOX_ELF32_EHDR;

................................
................................

#endif // _ZLOX_ELF_H_


    上面的ZLOX_ELF32_EHDR结构体就表示ELF header部分,第一个字段e_ident是一个包含16个字节的数组,其中e_ident[0]到e_ident[3]这4个字节用于标识当前的文件是否是一个有效的ELF可执行文件,这4个字节的值依次是0x7f、'E'、'L'、'F'。

    e_ident[4]用于表示当前硬件系统的位数:1表示32位,2表示64位。e_ident[5]表示数据的存储方式:1表示little endian小字节序,2表示big endian大字节序,e_ident[6]表示与ELF相关的版本号信息,当前必须为1,e_ident[7]和e_ident[8]两个字节都是和ABI相关的东东(可以使用man elf命令查看详情),剩下的e_ident[9]到e_ident[15]这几个字节都是保留的(为将来ELF扩充用的),目前全部用0填充。

    下图是cpuid可执行文件中和e_ident字段相关的二进制数据(可以使用vim编辑器,通过%!xxd命令切换到二进制格式来查看,要切换回去则使用%!xxd -r命令):


图2

    上面的0x7f、0x45(字符"E"的ASCII码)、0x4c(字符"L"的ASCII码)、0x46(字符"F"的ASCII码)前4个字节用于标识该文件是一个ELF文件,第5个字节0x01表示当前ELF的二进制指令适合在32位系统中运行,第6个字节0x01表示数据是以little endian小字节序进行存储的,第7个字节0x01表示当前ELF的版本号,第8和第9两个和ABI相关的字节,以及ABI后面的保留字节都是0 。

    上面是ZLOX_ELF32_EHDR结构体的第一个e_ident字段里各个字节的含义,下面再介绍该结构体中其他字段的含义。

    第2个e_type字段表示当前ELF可执行文件的类型:1表示ELF是一个类似于cpuid.o的可重定位的目标文件,2表示ELF是一个类似于cpuid的标准的可执行程序,3表示ELF是一个类似于libc.so.6的共享库文件。

    第3个e_machine字段表示当前ELF可以运行的体系结构:1表示AT&T WE 32100,2表示Sun Microsystems SPARC,3表示Intel 80386,4表示Motorola 68000,5表示Motorola 68000,7表示Intel 80860,8表示MIPS RS3000等等,这里的值是参考我的系统里的man elf命令加上/usr/include/elf.h头文件里的信息得到的。

    第4个e_version字段用于标识文件的版本号信息,目前的有效值为1 。

    第5个e_entry字段表示程序在虚拟内存中主入口的线性地址,在创建进程并将程序的指令代码读取到虚拟内存中后,必须将EIP指向该地址,对于汇编程式,该入口地址通常是_start标签处,对于C程式,该入口地址通常是main函数的起始位置。

    第6个e_phoff字段表示上面图1里所示的program header table在文件中的字节偏移值。

    第7个e_shoff字段表示上面图1里所示的section header table在文件中的字节偏移值。

    第8个e_flags字段表示当前文件与处理器相关的特有的标志,由于目前还没有定义这类标志,所以该字段用0填充。

    第9个e_ehsize字段表示ELF header部分的尺寸大小(以字节为单位)。

    第10个e_phentsize字段表示program header table中单个元素的尺寸大小(以字节为单位),program header table其实是一个数组,该数组里可以存储多个program header,每个program header都具有相同的尺寸大小。

    第11个e_phnum字段表示program header table数组中所包含的program header的数目。

    第12个e_shentsize字段表示section header table中单个元素的尺寸大小(以字节为单位),section header table同样也是一个数组,该数组由多个section header组成,每个section header的尺寸大小都是相同的,每个section header中都存储了一个Segment的相关信息(比如Segment的名称,文件中的偏移值,虚拟内存里的线性地址等)。

    第13个e_shnum字段表示section header table数组里所包含的section header的数目。

    最后一个e_shstrndx字段表示和section header相关的字符串表的索引值,之所以要引入字符串表,是因为每个section header的名字字段里存储的其实是字符串表中的偏移值,通过名字字段的偏移值和字符串表才能找到该名字对应的字符串信息,所以有必要在一开始就确定好字符串表的位置,字符串表也是一个Segment,该Segment里存储了所有名字的字符串信息,要找到该Segment,就必须知道该Segment对应的section header,而e_shstrndx字段的索引值就是字符串表的section header在section header table数组中的索引值,这样就可以通过索引值先从数组里找到section header,再由section header找到所需的Segment 。


图3

    上图红框框内的就是cpuid的ELF header里除了第一个e_ident字段以外的其他字段的值,其中,0x02为e_type,表示ELF是标准的可执行文件格式,0x03为e_machine,表示ELF可以运行的体系结构是Intel 80386。0x01为e_version,表示版本号信息,这里为固定的1。0x08048074为e_entry,表示可执行文件在虚拟内存里的主入口的线性地址为0x8048074。

    0x34为e_phoff,表示program header table在文件中的字节偏移值为0x34(对应十进制为52)。0x194为e_shoff,表示section header table在文件中的字节偏移值为0x194(对应十进制为404)。0x00为e_flags,由于目前没有定义相关标志,所以为0。0x34为e_ehsize,表示ELF header的尺寸大小是0x34即十进制52个字节的大小。

    0x20为e_phentsize,表示program header table中单个元素的尺寸大小是0x20即十进制32个字节的大小。0x02为e_phnum,表示program header table数组中所包含的program header的数目是2 。

    0x28为e_shentsize,表示section header table中单个元素的尺寸大小是0x28即十进制40个字节的大小。0x08为e_shnum,表示section header table数组里所包含的section header的数目是8 。

    最后一个0x05为e_shstrndx,表示和section header相关的字符串表的索引值为5 ,也就是说section header table数组中索引值为5的section header里存储了字符串表所在的Segment的相关信息。

    Linux系统中,还可以通过在命令行里输入readelf命令来查看ELF header的相关信息:

$ readelf -a cpuid
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048074
  Start of program headers:          52 (bytes into file)
  Start of section headers:          404 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         8
  Section header string table index: 5

.......................................
.......................................
$


program header table

    上面介绍了和ELF header相关的内容,从上面的图1里,我们可以看到,ELF header下面是program header table数组,该数组可以通过ELF header里的第6个e_phoff字段来找到,program header table数组是由program header组成,每个program header也可以用一个结构体来表示,不过在zenglOX的当前版本里并没有用到program header,所以zlox_elf.h头文件里也就没有去定义program header的结构体,下面是man elf命令显示的32位下的program header结构体的定义:

$ man elf
..............................
..............................

typedef struct {
   uint32_t   p_type;
   Elf32_Off  p_offset;
   Elf32_Addr p_vaddr;
   Elf32_Addr p_paddr;
   uint32_t   p_filesz;
   uint32_t   p_memsz;
   uint32_t   p_flags;
   uint32_t   p_align;
} Elf32_Phdr;

..............................
..............................
$


    一个program header用于表示文件的哪部分数据可以加载到哪个线性地址,以及这些数据的尺寸大小信息等,虽然可以参照program header的数据来加载可执行文件,但是按照它的数据来加载时,会将ELF header头部结构等附加数据也加载到内存中,当前版本还不希望将这些不必要的数据给加载到内存里,只希望将程序运行所必须的代码指令和相关数据加载到内存,所以zenglOX的当前版本并没有用到program header 。

    虽然当前版本没用到program header,不过下面还是有必要对其进行介绍,因为以后可能会用到这些数据。

    上面man elf命令输出显示的Elf32_Phdr结构体就表示一个program header,该结构体里各个字段的含义如下:

    第1个字段p_type表示program header的类型:1表示对应数据是loadable(可加载的),2表示program header里包含了动态链接相关的信息,3表示program header里包含了interpreter(解释器)的路径等相关的信息,至于其他类型的值与含义,请参考man elf命令的输出信息以及/usr/include/elf.h头文件里的相关的宏定义。

    第2个字段p_offset表示要加载的数据在ELF文件里的起始字节偏移值。

    第3个字段p_vaddr表示数据需要加载到的虚拟内存线性地址。

    第4个字段p_paddr表示数据需要加载到的物理内存地址,默认情况下和p_vaddr的值相同,并且在BSD系统下,该字段不再使用,并被0填充。

    第5个字段p_filesz表示要加载的数据在文件中的尺寸大小(以字节为单位)。

    第6个字段p_memsz表示要加载的数据在内存中的尺寸大小(也是以字节为单位)。

    第7个字段p_flags表示要加载的数据的位掩码标志,这些标志说明了要加载的数据的属性,例如,p_flags的位0表示要加载的数据是否可执行,位1表示要加载的数据是否可写,位2表示要加载的数据是否可读。

    第8个字段p_align表示要加载的数据在内存里的对齐字节数。

    下图是cpuid可执行文件里和program header相关的二进制数据:


图4

    上面红框框里的数据是第一个program header,紧挨在后面的黄框框里的数据是第二个program header,这两个program header构成了program header table数组。

    下面以上面第一个红框框里的program header为例进行说明。

    0x01为p_type,表示program header对应的要加载的数据是loadable(可加载的),0x00为p_offset表示要加载的数据在文件里的起始字节偏移值为0,0x08048000为p_vaddr,表示文件里p_offset开始的数据,应该被加载到虚拟内存的0x8048000的线性地址处,紧跟其后,又是一个0x08048000,该数据为p_paddr,表示要加载到的物理内存地址,这里和虚拟内存地址相同。

    接着的0x97为p_filesz,表示这段数据在文件里的尺寸大小是0x97即十进制151个字节的大小,再接下来的0x97为p_memsz,表示这段数据被加载到内存后,在内存里的尺寸大小也是0x97,与文件里的尺寸大小一样。

    0x05为p_flags,对应的二进制格式为101,位0与位2都是1,表示这段数据被加载到内存后,是可读和可执行的。

    最后一个0x1000为p_align,表示这段数据在内存里是以0x1000来进行对齐(也就是页对齐的方式,一页也是0x1000即4096个字节)。

    这些program header信息,也可以通过readelf命令来查看到:

$ readelf -a cpuid
...........................................
...........................................

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0x00097 0x00097 R E 0x1000
  LOAD           0x000098 0x08049098 0x08049098 0x0002b 0x0002b RW  0x1000

...........................................
...........................................
$ 


section header table:

    program header table后面就是各种Segment(段),以及section header table数组,由于Segment是通过section header table来进行定位的,所以下面就对section header table进行介绍。

    section header table和program header table一样,也是数组,section header table数组是由section header组成的,每个section header也可以由C结构体来进行定义,在zenglOX里,该结构体也定义在zlox_elf.h的头文件里:

/*zlox_elf.h 和ELF可执行文件格式相关的定义*/

#ifndef _ZLOX_ELF_H_
#define _ZLOX_ELF_H_

.........................................
.........................................

typedef struct _ZLOX_ELF32_SHDR{
	ZLOX_ELF32_WORD	sh_name;
	ZLOX_ELF32_WORD	sh_type;
	ZLOX_ELF32_WORD	sh_flags;
	ZLOX_ELF32_ADDR	sh_addr;
	ZLOX_ELF32_OFF	sh_offset;
	ZLOX_ELF32_WORD	sh_size;
	ZLOX_ELF32_WORD	sh_link;
	ZLOX_ELF32_WORD	sh_info;
	ZLOX_ELF32_WORD	sh_addralign;
	ZLOX_ELF32_WORD	sh_entsize;
} ZLOX_ELF32_SHDR;

.........................................
.........................................

#endif // _ZLOX_ELF_H_


    上面的ZLOX_ELF32_SHDR结构体就代表一个section header,该结构体中各个字段的含义如下:

    第1个sh_name字段表示section header所对应的Segment(段)的名字,前面提到过,该字段的值只是表示实际的名字字符串在字符串表里的偏移值。

    第2个sh_type字段表示对应的Segment的类型:1表示PROGBITS类型,说明对应的Segment里存储的是程序中自定义的数据,例如代码段里的指令代码,以及数据段里的相关数据等。2表示SYMTAB类型,说明对应的Segment里存储的是和程序相关的符号表信息。3表示STRTAB类型,说明对应的Segment里存储的是字符串表,字符串表是很多字符串的集合,需要通过偏移值来找到表中所需的字符串。至于其他类型的值与含义,请参考man elf命令的输出信息以及/usr/include/elf.h头文件里的相关的宏定义。

    第3个sh_flags字段表示对应的Segment的位掩码标志:位0表示该Segment里的数据是否可写。位1表示该section header是否需要为对应的Segment分配内存,像程序的代码段,数据段之类的都需要驻留到内存里才能执行,不过,有些只起控制作用的section header,它们就不需要分配内存。位2表示对应的Segment里的数据是否是可以执行的机器指令码(例如代码段里的指令数据)。至于其他的位掩码含义,则请参考man elf命令。

    第4个sh_addr字段表示如果对应的Segment里的数据需要驻留到内存中的话,那么就需要将数据放置到sh_addr指定的虚拟内存线性地址处。

    第5个sh_offset字段表示对应的Segment数据在文件里的起始偏移值,上面图1里显示的各种Segment在文件里的位置,都是通过section header中sh_offset字段的值来确定的。

    第6个sh_size字段表示对应的Segment数据的尺寸大小(以字节为单位)。

    第7个sh_link字段表示和当前section header相关联的另一个section header的索引值,具体的解释需要根据sh_type字段的类型来决定。

    第8个sh_info字段包含与当前section header相关的额外信息,这些额外信息的具体含义,也是根据sh_type字段的类型来决定的。

    第9个sh_addralign字段表示对应的Segment数据在内存里的对齐限制,也就是说sh_addr字段对应的线性地址除以sh_addralign的值,余数必须为0 ,如果sh_addralign字段的值为0,则表示对应的Segment没有对齐限制。

    第10个sh_entsize字段:某些section header对应的Segment是由多个尺寸相同的元素组成的数组,例如符号表,sh_entsize字段的值就表示了单个数组元素的尺寸大小信息(以字节为单位),如果该字段的值为0,则说明对应的Segment不是由多个尺寸相同的元素组成的数组。

    section header table数组中的第一个section header是空的,即所有字段都被0填充,例如,cpuid的第一个section header的二进制数据如下图所示:


图5

    上图显示,第一个section header的40个字节全部被0填充(第一个section header的起始文件偏移值定义在ELF header里,这里的偏移值是0x194)。紧随其后的第二个section header的二进制数据如下图所示:
 

图6

    上图里,第1个0x1b为sh_name,表示当前section header的名字字符串在字符串表里的偏移值为0x1b即十进制值27。第2个0x01为sh_type,表示对应的Segment是PROGBITS类型,第3个0x06为sh_flags,0x06对应的二进制格式为110,位1和位2都是1,表示对应的Segment是需要驻留在内存里的,且其中包含了可以执行的机器指令码。第4个0x08048074为sh_addr,表示对应的Segment里的数据需要加载到0x8048074的线性地址。

    第5个0x74为sh_offset,表示Segment的数据在ELF文件中的偏移值为0x74。第6个0x23为sh_size,表示Segment数据的尺寸大小为0x23即十进制35个字节的大小。

    第7个0x00为sh_link,表示没有相关联的section header。第8个0x00为sh_info,表示没有额外信息。第9个0x04为sh_addralign,表示sh_addr对应的线性地址是4字节对齐的。最后一个0x00为sh_entsize,表示当前section header对应的Segment不是由多个尺寸相同的元素组成的数组。

    这些section header信息,也可以通过readelf命令来查看到:

$ readelf -a cpuid

...................................
...................................

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        08048074 000074 000023 00  AX  0   0  4
  [ 2] .data             PROGBITS        08049098 000098 00002b 00  WA  0   0  4
  [ 3] .stab             PROGBITS        00000000 0000c4 000090 0c      4   0  4
  [ 4] .stabstr          STRTAB          00000000 000154 000009 00      0   0  1
  [ 5] .shstrtab         STRTAB          00000000 00015d 000036 00      0   0  1
  [ 6] .symtab           SYMTAB          00000000 0002d4 0000d0 10      7   9  4
  [ 7] .strtab           STRTAB          00000000 0003a4 000033 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

...................................
...................................
$ 


    从上面输出可以看到,第一个索引值为0的section header是空的,全部被0填充,第二个索引值为1的section header,其名字在字符串表里对应的实际的字符串为".text",这是cpuid可执行程序的代码段,该代码段在文件里的偏移值为0x74,尺寸大小为0x23,代码段里的机器指令码需要被加载到0x8048074的线性地址处。还有个比较重要的需要加载到内存里的是".data"数据段,该数据段需要被加载到0x8049098的线性地址处。剩下的就是一些符号表,字符串表,字符串表中比较重要的就是上面索引值为5的".shstrtab"字符串表,该字符串表在文件里的偏移值为0x15d ,0x15d处的字符串表的内容如下图所示:


图7

    可以看到其中包含了".text",".data"之类的字符串,前面提到的第二个section header中的sh_name对应的偏移值为0x1b,该偏移值加上0x15d(字符串表的起始地址),得到的0x178就是上图中".text"字符串的偏移值。

    对于".text"代码段,它在ELF文件里的偏移值为0x74(上面已经提到过),该偏移位置处的二进制数据如下:
 

图8

    上面的二进制数据就是cpuid里实际需要执行的机器指令的字节码,可以使用objdump工具来进行对照:

$ objdump -d cpuid

cpuid:     file format elf32-i386


Disassembly of section .text:

08048074 <_start>:
 8048074:	b8 00 00 00 00       	mov    $0x0,%eax
 8048079:	0f a2                	cpuid  
 804807b:	bf 98 90 04 08       	mov    $0x8049098,%edi
 8048080:	89 5f 1c             	mov    %ebx,0x1c(%edi)
 8048083:	89 57 20             	mov    %edx,0x20(%edi)
 8048086:	89 4f 24             	mov    %ecx,0x24(%edi)
 8048089:	b8 00 00 00 00       	mov    $0x0,%eax
 804808e:	bb 98 90 04 08       	mov    $0x8049098,%ebx
 8048093:	cd 80                	int    $0x80

08048095 <hlt>:
 8048095:	eb fe                	jmp    8048095 <hlt>
$


    从上面的输出可以看到,左侧红色标出来的二进制数据和上面图8所示的二进制数据是一样的。

    以上就是和ELF格式相关的内容。

    在zlox_elf.c文件里的代码是和ELF可执行文件相关的函数,例如zlox_elf_check_file函数:

ZLOX_BOOL zlox_elf_check_file(ZLOX_ELF32_EHDR * hdr)
{
	if(!hdr) 
		return ZLOX_FALSE;
	if(hdr->e_ident[ZLOX_EI_MAG0] != ZLOX_ELFMAG0) {
		zlox_monitor_write("ELF Error:ELF Header EI_MAG0 incorrect.\n");
		return ZLOX_FALSE;
	}
	if(hdr->e_ident[ZLOX_EI_MAG1] != ZLOX_ELFMAG1) {
		zlox_monitor_write("ELF Error:ELF Header EI_MAG1 incorrect.\n");
		return ZLOX_FALSE;
	}
	if(hdr->e_ident[ZLOX_EI_MAG2] != ZLOX_ELFMAG2) {
		zlox_monitor_write("ELF Error:ELF Header EI_MAG2 incorrect.\n");
		return ZLOX_FALSE;
	}
	if(hdr->e_ident[ZLOX_EI_MAG3] != ZLOX_ELFMAG3) {
		zlox_monitor_write("ELF Error:ELF Header EI_MAG3 incorrect.\n");
		return ZLOX_FALSE;
	}
	return ZLOX_TRUE;
}


    上面的函数,会根据ELF header里e_ident数组中开头的4个字节,来判断当前要加载的文件是否是一个有效的ELF可执行文件。另外zlox_elf.c文件里还有一个zlox_elf_check_supported函数,该函数会间接调用zlox_elf_check_file函数,并通过ELF header里的其他字段来综合判断某个ELF文件是否可被执行。

    如果某个要加载的文件是一个有效的ELF可执行文件的话,就可以通过zlox_load_elf函数将ELF里需要加载的Segment数据加载到指定的线性地址处,该函数也位于zlox_elf.c文件中:

ZLOX_UINT32 zlox_load_elf(ZLOX_ELF32_EHDR *hdr,ZLOX_UINT8 * buf)
{
	ZLOX_ELF32_SHDR * shdr = (ZLOX_ELF32_SHDR *)((ZLOX_UINT32)hdr + hdr->e_shoff);
	ZLOX_ELF32_SHDR * shstr = &shdr[hdr->e_shstrndx];
	ZLOX_CHAR * strtab = (ZLOX_CHAR *)hdr + shstr->sh_offset;

	if(hdr->e_entry == ZLOX_NULL)
		return ZLOX_NULL;

	if(strtab == ZLOX_NULL)
		return ZLOX_NULL;

	ZLOX_UINT32 i,j;
	ZLOX_CHAR * nameOffset;
	ZLOX_ELF32_SHDR * codeSheader = ZLOX_NULL;
	ZLOX_ELF32_SHDR * tmpSheader = ZLOX_NULL;
	ZLOX_CHAR * codeName = ".text";
	for(i = 0; i < hdr->e_shnum; i++)
	{
		nameOffset = strtab + shdr[i].sh_name;
		if(shdr[i].sh_type == ZLOX_SHT_PROGBITS)
		{
			if(zlox_strcmp(codeName,nameOffset)==0)
			{
				codeSheader = &shdr[i];
				break;
			}
		}
	}

	if(codeSheader == ZLOX_NULL)
		return ZLOX_NULL;

	for(i = 0; i < hdr->e_shnum; i++)
	{
		if(shdr[i].sh_type == ZLOX_SHT_PROGBITS)
		{
			tmpSheader = &shdr[i];
			if(tmpSheader->sh_addr == ZLOX_NULL)
				continue;
			for( j = tmpSheader->sh_addr;
				j < (tmpSheader->sh_addr + tmpSheader->sh_size);
				j += 0x1000)
			{
				// General-purpose stack is in user-mode.
				zlox_alloc_frame_do( zlox_get_page(j, 1, current_directory), 0 , 1 );
			}

			zlox_memcpy((ZLOX_UINT8 *)tmpSheader->sh_addr,
					(ZLOX_UINT8 *)(buf + tmpSheader->sh_offset),tmpSheader->sh_size);
		}
	}

	return (ZLOX_UINT32)hdr->e_entry;
}


    上面的代码,会先找到字符串表,并将字符串表在内存中的起始偏移值设置到strtab变量里,然后根据section header里的sh_name字段来循环查找".text"的代码段,如果不存在代码段,则返回空指针。如果有代码段,就再进行一个循环,在该循环里将所有section header中可以加载到内存的Segment数据都加载到指定的线性地址处。在将ELF里需要加载的代码段,数据段等加载到指定的线性地址空间后,最后就可以将e_entry的主入口的线性地址进行返回。

    在zlox_elf.c文件的最后,还有一个zlox_execve函数,该函数会先根据参数filename从VFS虚拟文件系统中读取出文件的内容,在通过zlox_fork创建出一个新任务(或者叫进程)后,再根据文件内容,先用zlox_elf_check_supported函数来判断该文件是否是一个有效的ELF文件,如果是有效的ELF文件则调用zlox_load_elf函数来将ELF文件里的Segment数据加载到指定的线性地址。最后将主入口地址进行返回。

    在zlox_syscall.c和zlox_syscall.h文件中,添加了一个zlox_syscall_execve的系统调用,系统调用号目前为4 。该系统调用会最终调用上面的zlox_execve函数来加载ELF可执行文件,在加载完文件后,最后会将栈里存储的EIP值设置为ELF可执行文件在内存里的主入口的线性地址,这样系统调用0x80中断在用iret指令返回时,就会转到新进程的主入口去执行了:

static ZLOX_VOID zlox_syscall_handler(ZLOX_ISR_REGISTERS * regs)
{
	// Firstly, check if the requested syscall number is valid.
	// The syscall number is found in EAX.
	if (regs->eax >= num_syscalls)
		return;

	// Get the required syscall location.
	ZLOX_VOID * location = syscalls[regs->eax];

	ZLOX_UINT32 oldesp,newesp; 

	// We don't know how many parameters the function wants, so we just
	// push them all onto the stack in the correct order. The function will
	// use all the parameters it wants, and we can pop them all back off afterwards.
	ZLOX_SINT32 ret;

	asm volatile("mov %%esp, %0" : "=r"(oldesp));

	asm volatile (" \
	  push %1; \
	  push %2; \
	  push %3; \
	  push %4; \
	  push %5; \
	  call *%6; \
	  pop %%ebx; \
	  pop %%ebx; \
	  pop %%ebx; \
	  pop %%ebx; \
	  pop %%ebx; \
	" : "=a" (ret) : "D" (regs->edi), "S" (regs->esi), "d" (regs->edx), "c" (regs->ecx), "b" (regs->ebx), "0" (location));

	asm volatile("mov %%esp, %0" : "=r"(newesp));

	if(oldesp != newesp)
	{
		if(oldesp > newesp)
			regs = (ZLOX_ISR_REGISTERS *)((ZLOX_UINT32)regs - (oldesp - newesp));
		else
			regs = (ZLOX_ISR_REGISTERS *)((ZLOX_UINT32)regs + (newesp - oldesp));
	}

	if(regs->eax == ZLOX_SYSCALL_EXECVE && ret > 0)
	{
		regs->eip = ret;
	}

	regs->eax = ret;
}


    上面代码显示,在函数结束前,如果是ZLOX_SYSCALL_EXECVE系统调用,并且zlox_execve函数返回的主入口的线性地址不为0的话,就将regs->eip设置为ret即主入口地址,这样iret中断返回时,就会转到主入口地址处去执行了。

    zlox_kernel.c里就通过zlox_syscall_execve系统调用来执行cpuid程式:

/*zlox_kernel.c Defines the C-code kernel entry point, calls initialisation routines.*/

..........................................
..........................................

//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_MULTIBOOT * mboot_ptr, ZLOX_UINT32 initial_stack)
{

..........................................
..........................................

	// 切换到ring 3的用户模式
	zlox_switch_to_user_mode();

	zlox_syscall_monitor_write("I'm in user mode!\n");

	zlox_syscall_monitor_write("welcome to zenglOX v0.0.11! I will execve a ELF file\n");

	zlox_syscall_execve("cpuid");

	for(;;)
		;

	return 0;
}


    该版本在bochs里的运行情况如下:


图9

    有关其他文件中代码的变更情况,请参考github中的diff信息。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
 
上下篇

下一篇: zenglOX v1.0.0 shell(命令行程式及各种小工具)

上一篇: zenglOX v0.0.10 Keyboard(获取键盘的输入)

相关文章

zenglOX v1.4.0与v1.4.1 通过ATA驱动读写硬盘里的数据, BUG修复, VirtualBox与VMware的调试功能

zenglOX v3.2.0 USB v1.1, UHCI, USB KeyBoard, USB Mouse

zenglOX v1.2.0 ISO 9660文件系统

zenglOX v3.0.2 PNG(Portable Network Graphics) and BUG Fix

zenglOX v1.5.0 zenglfs文件系统, MBR主引导记录, fdisk分区工具及format磁盘格式化工具, file文件目录读写工具等

zenglOX v1.6.0 保护模式下, VGA图形模式驱动程式