操作系统实验2

Uncategorized
9.6k words

前言:操作系统系列实验的blog比较抽象,如有需要可以直接去搬我上传的项目直接用
毕竟这几篇都是汇报检查是我打的草稿,内容很乱
链接如下:
可运行项目文件直达

实验二:ELF可执行文件加载

注意!!!!
首先,确保修改makefile中的177,178行
推荐点击桌面的linktowork,将makefile——boshsrc中的project1中的文件,
替换你的实验目录中的两个文件(makefile和boshsrc)

1
2
GENERAL_OPTS := -O -Wall $(EXTRA_C_OPTS)
CC_GENERAL_OPTS := $(GENERAL_OPTS) -fno-stack-protector

任务一

1.修改Project1项目中的/GeekOS/elf.c文件:在函数Parse_ELF_Executable()中添加代码,
分析ELF格式的可执行文件(包括分析得出ELF文件头、程序头),获取可执行文件长度、代码段、数据段等信息,并打印输出。并且,填充Exe_Format数据结构中的值域。

解析:该任务我们只需要修改/GeekOS/elf.c文件中的Parse_ELF_Executable()函数,该函数的作用是解析ELF格式的可执行文件,获取可执行文件长度、代码段、数据段等信息,并打印输出。

首先,我们需要定义ELF头的结构体,并将其指针指向ELF文件头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/*
* ELF executable loading
* Copyright (c) 2003, Jeffrey K. Hollingsworth <hollings@cs.umd.edu>
* Copyright (c) 2003, David H. Hovemeyer <daveho@cs.umd.edu>
* $Revision: 1.29 $
*
* This is free software. You are permitted to use,
* redistribute, and modify it as specified in the file "COPYING".
*/

#include <geekos/errno.h>
#include <geekos/kassert.h>
#include <geekos/ktypes.h>
#include <geekos/screen.h> /* for debug Print() statements */
#include <geekos/pfat.h>
#include <geekos/malloc.h>
#include <geekos/string.h>
#include <geekos/elf.h>


/**
* From the data of an ELF executable, determine how its segments
* need to be loaded into memory.
* @param exeFileData buffer containing the executable file
* @param exeFileLength length of the executable file in bytes
* @param exeFormat structure describing the executable's segments
* and entry address; to be filled in
* @return 0 if successful, < 0 on error
*/
int Parse_ELF_Executable(char *exeFileData, ulong_t exeFileLength, struct Exe_Format *exeFormat)
//zh:解析ELF格式的可执行文件,获取可执行文件长度,代码段,数据段等信息,并打印输出
{
elfHeader* elf_head = (elfHeader*) exeFileData; //zh:定义一个指向ELF文件头的指针
programHeader* program_head = (programHeader*)(exeFileData + elf_head->phoff); //zh:定义一个指向程序头的指针

KASSERT(exeFileData); //zh:确保exeFileData不为空

exeFormat->numSegments = elf_head->phnum; //zh:填充Exe_Format数据结构中的numSegments值域
exeFormat->entryAddr = elf_head->entry; //zh:填充Exe_Format数据结构中的entryAddr值域

// 打印ELF文件头信息
Print("ELF Header:\n");
Print(" Type: %u\n", elf_head->type);
Print(" Machine: %u\n", elf_head->machine);
Print(" Version: %u\n", elf_head->version);
Print(" Program Header Offset: %u\n", elf_head->phoff);
Print(" Number of Program Headers: %u\n", elf_head->phnum);
Print(" Number of Section Headers: %u\n", elf_head->shnum);

/*
Print("ELF 头信息:\n");
Print(" 类型: %u\n", elf_head->type);
Print(" 机器: %u\n", elf_head->machine);
Print(" 版本: %u\n", elf_head->version);
Print(" 程序头偏移: %u\n", elf_head->phoff);
Print(" 程序头数量: %u\n", elf_head->phnum);
Print(" 节头数量: %u\n", elf_head->shnum);
*/

int i;
for (i = 0; i < elf_head->phnum; ++i) //zh:遍历所有的程序头
{
Print("Program Header %d:\n", i);
Print(" Offset in File: %u\n", program_head->offset);
Print(" Length in File: %u\n", program_head->fileSize);
Print(" Start Address: 0x%lx\n", program_head->vaddr);
Print(" Size in Memory: %u\n", program_head->memSize);
Print(" Protection Flags: %u\n", program_head->flags);
/*
Print("程序头 %d:\n", i);
Print(" 文件偏移: %u\n", program_head->offset);
Print(" 文件长度: %u\n", program_head->fileSize);
Print(" 起始地址: 0x%lx\n", program_head->vaddr);
Print(" 内存大小: %u\n", program_head->memSize);
Print(" 权限标志: %u\n", program_head->flags);
*/
//感谢刘某人提供的代码,不然在下结构体定义全忘完了
exeFormat->segmentList[i].offsetInFile = program_head->offset; //zh:填充Exe_Format数据结构中的offsetInFile值域
exeFormat->segmentList[i].lengthInFile = program_head->fileSize; //zh:填充Exe_Format数据结构中的lengthInFile值域
exeFormat->segmentList[i].startAddress = program_head->vaddr; //zh:填充Exe_Format数据结构中的startAddress值域
exeFormat->segmentList[i].sizeInMemory = program_head->memSize; //zh:填充Exe_Format数据结构中的sizeInMemory值域
exeFormat->segmentList[i].protFlags = program_head->flags; //zh:填充Exe_Format数据结构中的protFlags值域
++program_head;
}
return 0;
}

在GeekOS中,核心态运行可执行程序的流程主要包括以下几个关键步骤和函数调用关系。
以下是一个简化的流程图描述,展示了可执行程序在内核中加载和运行的过程:

可执行程序在内核中加载和运行的流程

  1. 系统调用:用户程序通过系统调用请求加载可执行文件。

    • 相关函数:Sys_Exec()
  2. 解析可执行文件

    • 调用 Parse_ELF_Executable() 函数解析ELF文件,获取文件头和程序头信息。
  3. 内存分配

    • 根据程序头信息分配内存,调用 AllocateMemory() 或类似函数为程序的代码段和数据段分配内存。
  4. 加载段到内存

    • 将可执行文件中的各个段(如代码段、数据段)加载到分配的内存空间中。
    • 可能使用的函数:LoadSegment()CopyToMemory()
  5. 设置进程控制块(PCB)

    • 初始化进程控制块,包括设置程序计数器、栈指针等。
  6. 调度进程

    • 调用调度器,将新的进程添加到就绪队列中,准备运行。
  7. 运行进程

    • 切换到用户态,开始执行新的进程。

流程图示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
用户程序发起请求
|
v
Sys_Exec() <--------------------- 用户程序调用
|
v
Parse_ELF_Executable()
|
v
AllocateMemory()
|
v
LoadSegment() <----- 读取段信息
|
v
设置PCB信息
|
v
调度器
|
v
切换到用户态
|
v
执行用户程序
  • 系统调用:用户程序通过系统调用进入内核态,发起加载请求。
  • 解析和加载:内核解析可执行文件,分配内存并加载段信息到内存中。
  • PCB初始化:在加载完成后,初始化进程控制块以准备调度。
  • 调度和运行:调度器管理进程的调度,并切换到用户态执行程序。

掌握GeekOS在核心态运行可执行程序的原理,绘制出可执行程序在内核中加载、运行的流程图(需反映关键函数的调用关系)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
用户程序发起请求
|
v
Sys_Exec() <--------------------- 用户程序调用
|
v
Parse_ELF_Executable()
|
v
AllocateMemory()
|
v
LoadSegment() <----- 读取段信息
|
v
设置PCB信息
|
v
调度器
|
v
切换到用户态
|
v
执行用户程序

习题一

说明

1.在程序中输出字符串为什么要通过中断实现? 直接调用 Print 函数是否可行?

1
2
3
4
5
6
7
void ELF_Print(char*msg){
__asm__ __volatile__(
"int $0x90"
:/*no return */
:"a"(msg)//goes in eax
);
}

使用了0x90号中断,这个中断是用来输出字符串的,msg是要输出的字符串的地址,这个地址是在用户程序的地址空间中的,所以这个地址是虚拟地址,需要通过中断转换为物理地址,然后再输出到屏幕上。这个中断的实现是在内核中的,所以用户程序要输出字符串必须通过中断来实现。直接调用Print函数是不可行的,因为Print函数是在内核中的,用户程序不能直接调用内核中的函数,必顋通过中断来实现。

在程序中输出字符串时,使用中断实现的原因主要与操作系统的结构和硬件交互有关。

ai回答:

1. 中断实现的原因

  • 用户态与内核态的切换:在现代操作系统中,用户程序运行在用户态,而打印输出通常需要访问硬件(如显示器、串口等),这需要在内核态下进行。通过中断,可以安全地进行用户态到内核态的切换,确保系统的安全性和稳定性。

  • 资源管理:输出设备通常是共享资源,使用中断可以让操作系统更好地管理这些资源,避免多个进程同时访问同一个设备而导致冲突。

  • 异步操作:中断允许操作系统在处理输出时,可以同时处理其他任务。例如,打印操作可能会涉及等待设备准备好,使用中断可以避免阻塞程序执行。

2. 直接调用 Print 函数的可行性

  • 在内核态可行:如果你在内核态中直接调用 Print 函数,通常是可行的,因为此时已经在内核的上下文中,不需要通过中断进行切换。

  • 在用户态不可行:如果在用户态直接调用 Print 函数,通常会导致访问被禁止的内存区域或触发保护错误,因为用户态程序没有权限直接访问硬件资源。

总结

在程序中输出字符串时,使用中断是为了实现安全和高效的用户态与内核态之间的切换,以及对共享资源的管理。虽然在内核态下直接调用 Print 函数是可行的,但在用户态中则必须通过系统调用和中断来完成。这种设计增强了系统的安全性和稳定性。

习题二

2.在当前配置环境下,第2句并不能正常显示,分析其原因并使用GDB或ddd验证。
参考答案:这个变量是带有初始值的局部变量,根据编译程序对不同存储类型变量的空间分配规则,局部变量被存储在栈上,并利用可执行代码中的立即数进行初始化。使用反汇编工具可以验证这段程序对应的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
1029:c744241848692021 movl $0x21206948,0x18(8esp)
1031: c744241c 20546869 movl $0x69685420,0x1c(8esp)
1039:c744242073206973 movl $0x73692073,0x20(8esp)
1041: c744242420746865 movl $0x65687420,0x24(8esp)
1049:c744242820736563 movl $0x63657320,0x28(8esp)
1051:c744242c 6f 6e 6420 movl $0x20646e6f,0x2c(8esp)
1059: c744243073747269 movl $0x69727473,0x30(8esp)
1061:c74424346e 670a 00 movl $0xa676e,0x34(%esp)
1069: c744243800000000 movl $0x0,0x38(8esp)
1071: c744243c 00000000 movl $0x0,0x3c(8esp)

可见,这个字符串被存放到内存中的esp+0x18到esp+0x40区间。而在加载过程中虽然为该程序分配了栈页,但却没有切换,这个程序仍然使用内核栈。
打印程序中是通过该线程分配的内存起始地址加上偏移量来查找字符串的,而第二个字符串根本就不在线程的内存空间内,因此是无法输出这个字符串的。

反编译kernel.exe

1
objdump -d geekos/kernel.exe > a.asm

此处为a.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* A simple program for testing ELF parsing and loading
*
* Copyright (c) 2004, David H. Hovemeyer <daveho@cs.umd.edu>
* Copyright (c) 2004, Iulian Neamtiu <neamtiu@cs.umd.edu>
* $Revision: 1.24 $
*
* This is free software. You are permitted to use,
* redistribute, and modify it as specified in the file "COPYING".
*/

/* ELF_Print only prints NULL-terminated strings;
* no formatting or other fancy features */
void ELF_Print(char* msg);


char s1[40] = "Hi ! This is the first string\n";
//需要打印出s2,就在这定义成全局变量
//char s2[40] = "Hi ! This is the second string\n";

int main(int argc, char** argv)
{
char s2[40] = "Hi ! This is the second string\n";
//记得注释掉这的s2[40]

ELF_Print(s1);
ELF_Print(s2);

return 0;
}

此处为kernel.exe的部分反汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
10266:	89 54 24 34          	mov    %edx,0x34(%esp)
1026a: 89 44 24 30 mov %eax,0x30(%esp)
1026e: 8b 43 2c mov 0x2c(%ebx),%eax
10271: 89 44 24 2c mov %eax,0x2c(%esp)
10275: 8b 43 3c mov 0x3c(%ebx),%eax
10278: 89 44 24 28 mov %eax,0x28(%esp)
1027c: 8b 43 38 mov 0x38(%ebx),%eax
1027f: 89 44 24 24 mov %eax,0x24(%esp)
10283: 8b 43 34 mov 0x34(%ebx),%eax
10286: 89 44 24 20 mov %eax,0x20(%esp)
1028a: 8b 43 10 mov 0x10(%ebx),%eax
1028d: 89 44 24 1c mov %eax,0x1c(%esp)
10291: 8b 43 14 mov 0x14(%ebx),%eax
10294: 89 44 24 18 mov %eax,0x18(%esp)
10298: 8b 43 18 mov 0x18(%ebx),%eax
1029b: 89 44 24 14 mov %eax,0x14(%esp)
1029f: 8b 43 1c mov 0x1c(%ebx),%eax
102a2: 89 44 24 10 mov %eax,0x10(%esp)
102a6: 8b 43 20 mov 0x20(%ebx),%eax
102a9: 89 44 24 0c mov %eax,0xc(%esp)
102ad: 8b 43 24 mov 0x24(%ebx),%eax
102b0: 89 44 24 08 mov %eax,0x8(%esp)
102b4: 8b 43 28 mov 0x28(%ebx),%eax
102b7: 89 44 24 04 mov %eax,0x4(%esp)
102bb: c7 04 24 74 cd 01 00 movl $0x1cd74,(%esp)
102c2: e8 f5 0c 00 00 call 10fbc <Print>
102c7: 8b 43 38 mov 0x38(%ebx),%eax
102ca: 83 e0 03 and $0x3,%eax
102cd: 83 f8 03 cmp $0x3,%eax

在汇编代码片段中,可以分析以下几个关键点:

汇编代码分析

  1. mov 指令
    代码中大量使用了 mov 指令,这些指令将寄存器或内存中的值移动到栈上的特定位置。这是将数据准备好,以便随后调用 Print 函数。

    • 例如:
      1
      mov    %edx,0x34(%esp)
      这条指令将寄存器 %edx 的值存储到栈顶偏移量为 0x34 的位置。
  2. 准备参数
    这些 mov 指令的目的显然是将多个参数传递给 Print 函数。函数的参数通常通过栈传递,因此这里在栈上预留了空间并放入要打印的数据。

  3. 调用 Print 函数
    在代码中,出现了对 Print 函数的调用:

    1
    call   10fbc <Print>

    这行代码表明即将调用 Print 函数,函数的参数已经通过栈准备好。

  4. 字符串的内存地址
    mov 指令的最后几条中,有一条指令:

    1
    movl   $0x1cd74,(%esp)

    这里将一个立即数(可能是字符串的地址)移动到栈顶。这个地址对应于待打印字符串的内存位置。

  5. 条件判断
    最后,代码使用了 cmpjne 指令进行条件判断,以决定下一步的操作。这可能用于检查 Print 函数调用后的状态。

潜在问题分析

第二句字符串不能正常显示的原因可能是:

  • 字符串地址问题:如果 0x1cd74 指向的地址不在当前线程的栈空间内,打印操作将无法访问到该字符串,导致输出异常或错误。

  • 栈使用问题:如果在程序执行中没有正确切换到用户栈,可能会导致尝试访问的地址无效,从而引发访问错误。

  • 参数数量不匹配:如果 Print 函数期望的参数数量与实际提供的不匹配,也可能导致未定义的行为。

验证步骤

为了进一步验证这些问题,你可以:

  1. 检查 0x1cd74 地址:确保该地址确实存储了有效字符串。可以使用 GDB 或 ddd 查看该地址内容:

    1
    x/s 0x1cd74
  2. 验证栈空间:确保在调用 Print 前,程序已经正确切换到用户栈,并且栈空间已分配。

  3. 调试打印:在调用 Print 之前,打印出相关变量和寄存器的值,确保它们的内容都是预期的。

通过这些步骤,可以帮助确认第二句字符串无法正常显示的根本原因,并进行相应的修正。
//如何验证栈空间是否已经切换到用户栈,以及栈空间是否已分配?
:在调用Print函数之前,可以通过打印栈指针esp的值来验证栈空间是否已经切换到用户栈,并且栈空间是否已分配。在用户程序中,esp指向的地址应该在用户栈的范围内,而不是内核栈的范围内。如果esp指向的地址在用户栈的范围内,说明栈空间已经切换到用户栈,并且栈空间已分配。如果esp指向的地址在内核栈的范围内,说明栈空间没有切换到用户栈,或者栈空间没有分配。通过这种方式,可以验证栈空间是否已经切换到用户栈,以及栈空间是否已分配。

ddd不会用,摆

Comments