GeekOS之旅-Project1 (Parse an ELF executable Files) 与 ELF 文件格式的浅析

本文详细介绍了如何理解并解析ELF文件格式,特别是针对Geekos库中提供的Parse_ELF_Executable函数。通过分析ELF头部和程序头部表,解释了如何获取必要的文件段信息,并展示了解析过程中的关键步骤和代码实现。最后,通过实例分析了如何使用提供的代码片段来加载和解析ELF文件。

想知道这个Project的Assignment之前,我们首先需要把这个Project编译过并把bochs启动起来. 编译和前一个Project一样很快通过.但是bochs启动遇到一个错误

00000000000p[ ] >>PANIC<< .bochsrc:10: directive 'diskc' not understood

看了下 .boshrc 中有这样一句: 10 diskc: file=diskc.img, cyl=40, heads=8, spt=64

看了下刚才编译过的project1里面有个disk.img ,这个diskc.img是什么呢? 其实diskc.img是个硬盘映像,我们之所以要加载这个是因为我们下面肯定会用到. 所谓映像是原始设备的对应字节. 所以需要在.bochrc中配置 disk.img.

再.boshrc添加如下

ata0-master:type=disk, mode=flat, path=./diskc.img, cylinders=40, heads=8, spt=64这句话就是制定硬盘参数,type为(disk,cdrom) mode(flat<一个文件布局>, concat<多文件布局>, external, dll ....), path就是路径了, cylinders为柱面大小, heads为头部大小,spt为每磁道扇区数

这么一解释,也就清楚多了. 好加上去,并把 第10行注释掉. 然后启动.


启动成功! 可以看到这个Project的Assigment:Parse an ELF executable image.

仔细阅读附带的手册Project2 的 Required Reading 和Synopsis . 主要任务是实现 src/geekos/elf.c 的Parse_ELF_Executable() 这个函数. 函数的功能是读取ELF文件中的offset, length, user address for the executable's text and data segments . 然后fill in the Exe_Format中!

所以我们首先要进行了解ELF格式的文件. 分析的样本用 project1 user下提供的a.exe , 这个源码在 src/user下a.c .

可以从google中搜索 Elf format file来进行了解.这里 可以我们也可以探讨下ELF 文件:

-> ELF是Linux默认的可执行文件格式, ELF包含三种类型:可重定位的文件(比如.o 目标文件), 共享文件 , 可执行文件. ELF文件包含 ELF Header, Sections ,String Table , Symbol Table, Relocation(Relocation Types) 这几个重要的部分. 让我们大概来看下ELF文件的组织图.


上面第一个为连接视图, 第二个为执行视图, ELF header(ELF 头部) , Program header table(程序头表), Section header table(节头表)

我们从ELF头部的数据结构开始看起, 打开geekos/elf.c 看下面这个结构体

14 /* 15 * ELF header at the beginning of the executable. 16 */ 17 typedef struct { 18 unsigned char| ident[16]; //信息 19 unsigned short|type; // 文件类型 20 unsigned short|machine; //硬件体系 21 unsigned int| version; 22 unsigned int| entry; //程序入口点 23 unsigned int| phoff; //程序头部偏移量 24 unsigned int| sphoff; //节头部偏移量 25 unsigned int| flags; //处理器特定标志 26 unsigned short|ehsize; //ELF头部长度 27 unsigned short|phentsize; //程序头部一段的长度 28 unsigned short|phnum; //程序头部段的个数 29 unsigned short|shentsize; //section 头部中一段的长度 30 unsigned short|shnum; //section 段个数 31 unsigned short|shstrndx; //section 头部字符表索引 32 } elfHeader;里面的相关信息 我已经注释上去了. 我们接着用readelf -h工具查看下a.exe文件 来对照着看下

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: 0x1000 Start of program headers: 52 (bytes into file) Start of section headers: 4420 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 3 Size of section headers: 40 (bytes) Number of section headers: 7 Section header string table index: 4 可以对照的看下, 下面再研究 程序头表,继续看下它的结构体:

34 /* 35 * An entry in the ELF program header table. 36 * This describes a single segment of the executable. 37 */ 38 typedef struct { 39 unsigned int type; //段类型 40 unsigned int offset; //段位置相对于文件起始的offset 41 unsigned int vaddr; //段在内存中的地址 42 unsigned int paddr; //段的物理地址 43 unsigned int fileSize; //段在文件中的长度 44 unsigned int memSize; //段在内存中的长度 45 unsigned int flags; //标记 46 unsigned int alignment; //对齐标记 47 } programHeader;再用readelf -l 来看下a.exe的信息.

Elf file type is EXEC (Executable file) Entry point 0x1000 There are 3 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x001000 0x00001000 0x00001000 0x000a2 0x000a2 R E 0x1000 LOAD 0x0010c0 0x000020c0 0x000020c0 0x00028 0x00028 RW 0x1000 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 Section to Segment mapping: Segment Sections... 00 .text 01 .data 02

ELF深入可以看文档 看资料. 等提到ELF问题时 我们再来深入一下.

来做第一步 研究下Parse_ELF_Executable的原型.

int Parse_ELF_Executable(char *exeFileData, ulong_t exeFileLength, struct Exe_Format *exeFormat);exeFileData: 这个buffer包含了可执行的文件 也就是ELF文件.

exeFileLength: 这个是执行文件的长度

exeFormat: 这个结构体包含了文件的段和入口地址

返回值为int: ret返回0为成功

解析了elfHeader 和 ProgramHeader这两个结构体 剩下的就很轻松了, 分别用这两个结构体解析exeFileData的内容

exeFileData头部的指向肯定是从elfHeader开始的 因为之前我们已经分析过了,然后自带poff的属性 这是程序头部的偏移量,首地址+poff就可以得出programHeader的首地址了.

33 elfHeader* header = exeFileData; 34 programHeader* pHeader = (exeFileData+header->phoff);再继续看Exe_Format这个结构体.

77 /* 78 * A struct concisely representing all information needed to 79 * load an execute an executable. 80 */ 81 struct Exe_Format { 82 struct Exe_Segment segmentList[EXE_MAX_SEGMENTS]; /* Definition of segments */ 83 int numSegments;| | /* Number of segments contained in the executable */ 84 ulong_t entryAddr;| | /* Code entry point address */ 85 };
segmentList是个段的数组.

numSegments是段的数目

entryAddr是入口地址.

其中numSegments就是 elfHeader中的phnum, entryAddr是 elfHeader 的entry

segmentList因为是个数组,但是里面的每个元素是个结构体 我们先搞清这个结构体的内容:

57 /* 58 * A segment of an executable. 59 * It specifies a region of the executable file to be loaded 60 * into memory. 61 */ 62 struct Exe_Segment { 63 ulong_t offsetInFile;| /* Offset of segment in executable file */ 64 ulong_t lengthInFile;| /* Length of segment data in executable file */ 65 ulong_t startAddress;| /* Start address of segment in user memory */ 66 ulong_t sizeInMemory;| /* Size of segment in memory */ 67 int protFlags;| | /* VM protection flags; combination of VM_READ,VM_WRITE,VM_EXEC */ 68 };
稍微思考下, 这些不就是programHeader的offset, fileSize, vaddr, memSize, flags 嘛

ok一切准备就绪,就剩下苦力把程序写上去了.

30 int Parse_ELF_Executable(char *exeFileData, ulong_t exeFileLength, 31 struct Exe_Format *exeFormat) 32 { 33 elfHeader* header = exeFileData; 34 programHeader* pHeader = (exeFileData+header->phoff); 35 exeFormat->numSegments = header->phnum; 36 exeFormat->entryAddr = header->entry; 37 int i = 0; 38 for (; i< header->phnum; i++) { 39 exeFormat->segmentList[i].offsetInFile = pHeader->offset; 40 exeFormat->segmentList[i].lengthInFile = pHeader->fileSize; 41 exeFormat->segmentList[i].startAddress = pHeader->vaddr; 42 exeFormat->segmentList[i].sizeInMemory = pHeader->memSize; 43 exeFormat->segmentList[i].protFlags = pHeader->flags; 44 pHeader++; 45 } 46 47 return 0; //!! 48 49 //TODO("Parse an ELF executable image"); 50 }
整个程序如上,需要注意的是 return 0; 别忘了放进去.
测试结果如下:


下面继续 Project 2.!


一、项目设计目的\n扩充GeekOS操作系统内核,使得系统能够支持用户级进程的动态创建和执行。\n\n二、项目设计提示\n1GeekOS进程状态及转换\n\nGeekOS系统最早创建的内核进程有Idle、Reaper和Main三个进程,它们由Init_Scheduler函数创建:最先初始化一个核态进程mainThread,并将该进程作为当前运行进程,函数最后还调用Start_Kernel_Thread 函数创建了两个系统进程Idle和Reaper。 所以,Idle、Reaper和Main三个进程是系统中最早存在的进程。\n\n2、GeekOS的用户态进程\n在GeekOS中为了区分用户态进程和内核进程,在Kernel_Thread结构体中设置了一个字段 userContext,指向用户态进程上下文。对于内核进程来说,这个指针为空,而用户态进程都拥有自己的用户上下文(User_Context)。因此,在GeekOS中要判断一个进程是内核进程还是用户态进程,只要通过userContext字段是否为空来判断就可以了。\n\n\n3、用户态进程创建流程\n\n\nSpawn函数的功能\nint Spawn(const char *program, const char *command, struct Kernel_Thread **pThread)\n参数说明:Program对应的是要读入内存缓冲区的可执行文件,Command是用户执行程序执行时的命令行字符串,pThread是存放指向刚创建进程的指针。\nSpawn函数主要完成的主要功能是:\n(1)调用Read_Fully函数将名为program的可执行文件全部读入内存缓冲区。\n(2)调用Parse_ELF_Executable函数,分析ELF格式文件。Parse_ELF_Executable函数功能在项目1中已经实现。\n(3)调用Load_User_Program将可执行程序的程序段和数据段等装入内存,初始化User_context数据结构。\n(4)调用Start_User_Thread函数创建一个进程并使该进程进入准备运行队列。\n\nLoad_User_Program函数\nLoad_User_Program函数在“/src/geekos/userseg.c”文件中实现,代码也需要开发人员自己完成,函数原型如下:\nint Load_User_Program(char *exeFileData, ulong_t exeFileLength,\nstruct Exe_Format *exeFormat, const char *command,\nstruct User_Context **pUserContext)\n/* 参数说明:\nexeFileData——保存在内存缓冲中的用户程序可执行文件;\nexeFileLength——可执行文件的长度;\nexeFormat——调用Parse_ELF_Executable函数得到的可执行文件格式信息;\ncommand——用户输入的命令行,包括可执行文件的名称及其他参数;\npUserContext——指向User_Conetxt的指针,是本函数完成用户上下文初始化的对象\n*/\nLoad_User_Program主要实现功能如下:\n(1)根据Parse_ELF_Executable函数的执行结果Exe_Format中的Exe_Segment结构提供的用户程序段信息,用户命令参数及用户态进程栈大小计算用户态进程所需的最大内存空间,即要分配给用户态进程的内存空间。\n(2)为用户程序分配内存空间,并初始化。\n(3)根据Exe_Segment提供的用户段信息初始化代码段、数据段以及栈段的段描述符和段选择子。\n(4)根据段信息将用户程序中的各段内容复制到分配的用户内存空间。\n(5)根据Exe_Format结构初始化User_Context结构中的用户态进程代码段入口entry字段,并根据command参数初始化用户内存空间中的参数块。\n(6)初始化User_Context结构的用户打开文件列表,并添加标准输入输出文件。\n(7)将初始化完毕的User_Context指针赋予*pUserContext,返回0表示成功。\n\n4、 用户态进程空间\n每个用户态进程都拥有属于自己的内存段空间,如:代码段、数据段、栈段等,每个段有一个段描述符(segment descriptor),并且每个进程有一个段描述符表(Local Descriptor Table),用于保存该进程的所有段描述符。操作系统中还设置一个全局描述符表(GDT,Global Descriptor Table),用于记录了系统中所有进程的ldt描述符。\n\n\n5、用户态进程创建LDT的步骤\n(1)调用函数Allocate_Segment_Descriptor()新建一个LDT描述符;\n(2)调用函数Selector()新建一个LDT选择子;\n(3)调用函数Init_Code_Segment_Descriptor()初始化一个文本段描述符;\n(4)调用函数Init_Data_Segment_Descriptor()初始化一个数据段描述符;\n(5)调用函数Selector()新建一个数据段选择子;\n(6)调用函数Selector()新建一个文本(可执行代码)段选择子。\n\n三、项目2的实现:\n1)“src/GeekOS/user.c”文件中的函数Spawn(),其功能是生成一个新的用户级进程;\n\nint Spawn(const char *program, const char *command, struct Kernel_Thread **pThread) { \n int res; \n\n /* 读取 ELF 文件 */ \n char *exeFileData = NULL; \n ulong_t exeFileLength = 0; \n res = Read_Fully(program, (void**)&exeFileData, &exeFileLength); \n if (res != 0) \n { \n if (exeFileData != NULL) Free(exeFileData); \n return ENOTFOUND; \n } \n\n /* 分析 ELF 文件 */ \n struct Exe_Format exeFormat; \n res = Parse_ELF_Executable(exeFileData, exeFileLength, &exeFormat); \n if (res != 0) \n { \n if (exeFileData != NULL) Free(exeFileData); \n return res; \n }\n\n /* 加载用户程序 */ \n struct User_Context *userContext = NULL; \n res = Load_User_Program(exeFileData, exeFileLength, &exeFormat, command, &userContext); \n if (res != 0) \n { \n if (exeFileData != NULL) Free(exeFileData); \n if (userContext != NULL) Destroy_User_Context(userContext); \n return res; \n } \n if (exeFileData != NULL) Free(exeFileData); \n exeFileData = NULL; \n\n /* 开始用户进程 */ \n struct Kernel_Thread *thread = NULL; \n thread = Start_User_Thread(userContext, false); \n /* 超出内存 创建新进程失败 */ \n if (thread == NULL) \n { \n if (userContext != NULL) Destroy_User_Context(userContext);\n return ENOMEM; \n }\n\n KASSERT(thread->refCount == 2);\n /* 返回核心进程的指针 */ \n *pThread = thread; \n return 0; \n}\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n2)“src/GeekOS/user.c”文件中的函数Switch_To_User_Context(),调度程序在执行一个新的进程前调用该函数以切换用户地址空间;\n\nvoid Switch_To_User_Context(struct Kernel_Thread* kthread, struct Interrupt_State* state)\n{\n /*\n * Hint: Before executing in user mode, you will need to call\n * the Set_Kernel_Stack_Pointer() and Switch_To_Address_Space()\n * functions.\n */\n\n //之前最近使用过的 userContxt\n static struct User_Context* s_currentUserContext;\n\n //指向User_Conetxt的指针,并初始化为准备切换的进程\n struct User_Context* userContext = kthread->userContext;\n\n KASSERT(!Interrupts_Enabled());\n\n //userContext为0表示此进程为核心态进程就不用切换地址空间\n if (userContext == 0) return;\n\n if (userContext != s_currentUserContext)\n {\n //为用户态进程时则切换地址空间\n Switch_To_Address_Space(userContext);\n //新进程的核心栈指针 \n ulong_t esp0 = ((ulong_t)kthread->stackPage) + PAGE_SIZE;\n //设置内核堆栈指针\n Set_Kernel_Stack_Pointer(esp0);\n //保存新的 userContxt\n s_currentUserContext = userContext;\n }\n}\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n3)“src/GeekOS/elf.c”文件中的函数Parse_ELF_Executable()。该函数的实现要求和项目1相同。\n4)“src/GeekOS/userseg.c”文件中主要是实现一些为实现对“src/GeekOS/user.c”中高层操作支持的函数。\nCreate_User_Context()函数,用于创 建并初始化一个用户上下文结构。
11-24
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值