-
Notifications
You must be signed in to change notification settings - Fork 210
fix zcore bug
zCore 操作系统目前的核心代码已经接近3万行代码,如何发现并修补zCore的漏洞越来越成为一个值得重视的事情。本文将描述近期在修补zCore操作系统的过程,包括建立CI/CD, 选择测试用例,分析测试用例,分析出错情况,修补内核等。希望能帮助zCore的开发者改进和修补zCore操作系统。
zCore操作系统是一种用 Rust 语言重新实现的 C++ based Zircon 微内核,并扩展了LibOS模式和Linux模式。zCore操作系统可以在用户态和内核态运行Linux应用和Fuchsia应用。随着zCore的功能发展,代码量越来越大,如何发现、测试和修复zCore中的bug也越来越有挑战。目前zCore驻留在GITHUB上,而GITHUB有自动的CI/CD支持。为此,我们想到充分利用GITHUB的CI/CD来帮助我们自动测试zCore的功能是否正常。为此从2020年6月开始,王润基逐步开展了基于zCore的用户态LibOS模式来自动测试zCore操作系统。这部分的测试代码主要在:
# github CI/CD自动测试的总控脚本(会调用下面的测试脚本)
/.github/workflows/main.yml b/.github/workflows/main.yml
# 测试 zircon syscall的测试脚本(基于zircon的core-tests测试集)
/scripts/core-tests.py
# 通过的core-tests测试点列表(基于zircon的core-tests测试集)
/scripts/zircon/test-check-passed.txt
# core-tests测试集列表(基于zircon的core-tests测试集)
/scripts/zircon/testcased.txt
# 测试 linux syscall的测试脚本(基于musl libc的libc-tests测试集)
/scripts/libc-tests.py
# 通过的libc-tests测试集列表 (在zcore LibOS模式下通过的libc-tests测试集)
/scripts/linux/test-result.txt
# 没通过的libc-tests测试集列表 (在zcore LibOS模式下没通过的libc-tests测试集)
/scripts/linux/test-allow-failed.txt
开发者在对zCore仓库的每次提交时,上述这部分测试功能通过GITHUB的CI/CD的管理被自动触发。并且只有在通过了测试(没有引入新bug)的情况下,才能被merge到zCore仓库的master
分支。这样在一定程度上有效地杜绝了潜在bug的任意引入。但上述测试功能的一个不足是没有加入对基于内核态的zCore的功能测试,而zCore已经加入了对RISC-V 64 CPU
的支持,这导致测试的范围受限,这需要改进上述测试脚本。于是在2021年开源操作系统夏令营期间,具体时间段为2021.08.08~2021.08.10,我们进行了一次给内核态的zCore提供自动测试和find&fix bug的实战尝试。
于是我们开始进行对zCore的CI/CD进行扩展。实现内核态zCore的测试,需要有一个模拟物理机的虚拟机软件(我们采用的QEMU)。参考zCore现有Makefile
中的一些运行脚本,我们首先需要让zCore通过某种方式很方便地支持启动不同的应用程序。这其实在zCore中已经有所考虑了。以x86-64的硬件环境为例,zCore支持基于UEFIbootloader
即rboot
的启动,而rboot
会读取rboot.conf
文件中的内容,并把信息传递给zCore kernel。zCore kernel启动后,就可以读到rboot.conf中的内容。所以我们每次把要测试的用例路径名放到这个文件(位于/zCore/target/x86_64/release/esp/EFI/Boot/rboot.conf
)中,重启QEMU,就可以在不修改zCore 内核和文件系统的情况下,快速测试不同的测试用例了。
rboot.conf文件格式
...
# LOG=debug/info/error/warn/trace
# add ROOTPROC info ? split CMD and ARG : ROOTPROC=/libc-test/src/functional/argv.exe? OR ROOTPROC=/bin/busybox?sh
cmdline=LOG=error:TERM=xterm-256color:console.shell=true:virtcon.disable=true:ROOTPROC=/libc-test/src/math/fmin.exe?
包含cmdline
这一行的内容是rboot要传递给zCore的命令行信息。“:
”是分割不同参数的分隔符。最后一部分ROOTPROC=/libc-test/src/math/fmin.exe?
表示的是要zCore执行的应用程序路径和它的参数。其中的ROOTPROC
是一个key,/libc-test/src/math/fmin.exe?
是用?
分隔的两个value。前面的value
表示应用程序路径,后面的value
表示应用程序的参数。 \zCore\main.rs
中的get_rootproc
函数完成对应程序路径和参数的解析。如果cmdline这一行没有ROOTPROC
,那么zCore将缺省执行/bin/busybox sh
命令。
有了rboot.conf文件和zCore的识别后,就算完成了让zCore支持执行不同应用的关键部分。接下来要做的就是写测试脚本了。参考已有的\scripts\libc-test.py
的大致思路,我们可以写出\scripts\baremetal\libc-test.py
代码:
......
TIMEOUT = 10 # seconds
ZCORE_PATH = '../zCore'
BASE = 'linux/'
CHECK_FILE = BASE + 'baremetal-test-allow.txt'
FAIL_FILE = BASE + 'baremetal-test-fail.txt'
RBOOT_FILE = 'rboot.conf'
RESULT_FILE ='../stdout-zcore'
# 用rboot字符串表示的rboot.conf文件的内容
rboot= r'''
# The config file for rboot.
# Place me at \EFI\Boot\rboot.conf
......
# LOG=debug/info/error/warn/trace
# add ROOTPROC info ? split CMD and ARG : ROOTPROC=/libc-test/src/functional/argv.exe? OR ROOTPROC=/bin/busybox?sh
cmdline=LOG=error:TERM=xterm-256color:console.shell=true:virtcon.disable=true:ROOTPROC='''
......
# 判断zCore或应用执行失败的字符串
FAILED = [
"failed", # 应用产生的
"ERROR", # 内核产生的
]
# 获得要测试的文件列表
with open(CHECK_FILE, 'r') as f:
allow_files = set([case.strip() for case in f.readlines()])
# 获得还没测试通过的文件列表。在下面的循环测试中,避免测试该列表中的文件
with open(FAIL_FILE,'r') as f:
failed_files = set([case.strip() for case in f.readlines()])
# 基于QEMU模拟器,循环更新rboot.conf,并测试zCore,把输出结果放到stdout-zcore文件中,扫描该是否有执行失败的字符串
for file in allow_files:
if not (file in failed_files):
rboot_file=rboot+file+'?'
with open(RBOOT_FILE,'w') as f:
print(rboot_file, file=f)
try:
subprocess.run(r'cp rboot.conf ../zCore && cd ../ && make baremetal-test | tee stdout-zcore '
r'&& '
r'sed -i '
r'"/BdsDxe/d" stdout-zcore',
shell=True, timeout=TIMEOUT, check=True)
with open(RESULT_FILE, 'r') as f:
output=f.read()
break_out_flag = False
for pattern in FAILED:
if re.search(pattern, output):
failed.add(file)
break_out_flag = True
break
if not break_out_flag:
passed.add(file)
except subprocess.CalledProcessError:
failed.add(file)
except subprocess.TimeoutExpired:
timeout.add(file)
# 统计最后的测试结果
......
print("Total tested num: ", len(allow_files)-len(failed_files))
......
# 如果有3个以上的测例没过,该次测试返回-1,表示测试失败
if len(failed) > 3 :
sys.exit(-1)
else:
sys.exit(0)
在已有.github\workflow\rustc20210727.yml
中,添加如下内容,就可以实现在QEMU上执行 zCore测试的自动化CI/CD。增加的部分如下:
baremetal-libc-test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2021-07-27
components: rust-src
- name: Pull prebuilt images
run: git lfs pull -I prebuilt/linux/libc-libos.so
- name: Install musl toolchain qemu-system-x86
run: sudo apt-get install musl-tools musl-dev qemu-system-x86 -y
- name: Prepare rootfs and libc-test
run: make baremetal-test-img
- name: Build kernel
run: cd zCore && make build mode=release linux=1 arch=x86_64
- name: create qemu disk
run: cd zCore && make baremetal-qemu-disk mode=release linux=1 arch=x86_64
- name: Run baremetal-libc-test
run: |
cd scripts
python3 ./baremetal-libc-test.py
这里面的20~21
行是为了以后测试zCore中文件系统对文件的读写准备的,目前其实没有用到。这里面的大致执行流程的含义写在了uses
和name
中。具体的步骤是:
-
runs-on: ubuntu-20.04
:在ubuntu-20.04 x86-64上运行测试 -
uses: actions/checkout@v2
:获取zCore repo -
uses: actions-rs/toolchain@v1
:安装rustc-nightly-2021-07-27工具链,包含rust-src
component -
name: Pull prebuilt images
:下载预编译的fs镜像,和用于LibOS模式的定制libc-libos.so (这里的测试其实用不上) -
name: Install musl toolchain qemu-system-x86
:安装编译musl libc-tests测试用例的工具和QEMU工具 -
name: Prepare rootfs and libc-test
:构建rootfs,编译libc-tests测试用例,并放到rootfs中 -
name: Build kernel
:编译zCore kernel -
name: create qemu disk
:建立一个虚拟盘,用于文件相关的测试(目前的测试没有用) -
name: Run baremetal-libc-test
:执行测试脚本,测试libc-test测试用例
这样,就建立好了基于GITHUB的CI/CD自动测试。
通过对libc-test测试用例的测试结果分析,我们发现有大约100多个的“*-static.exe”测试用例在执行时都出错了。这促使我们希望看看是什么原因导致了这些错误。首先,要找出它们的共性特点。通过一些命令可以分析出这些文件的特点:
$ cd zCore
$ file rootfs/libc-test/src/functional/argv-static.exe
argv-static.exe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
可以看到,这类文件都是用静态库编译出来的静态可执行文件。即使是最简单的静态可执行文件都不能在QEMU上运行的zCore中正确执行。接下来,我们需要缩小bug的范围,而libc-test对于每个测试用例都生成了两个执行文件,一个是静态可执行文件,基于是基于动态库的动态可执行文件。比如:
- rootfs/libc-test/src/functional/argv-static.exe:静态可执行文件
- rootfs/libc-test/src/functional/argv.exe:动态可执行文件(通过file命令可以分析)
通过测试结果,我们进一步发现argv.exe
可以正常运行。于是,我们可以基本定位是zCore对静态可执行文件的支持有问题。接下来,我们会选择一个简单的测试用例来分析。argv.c
就是一个合适的例子:
#include <limits.h>
#include <stdio.h>
#include "test.h"
#define TEST(c, ...) \
( (c) || (t_error(#c " failed: " __VA_ARGS__),0) )
int main(int argc, char **argv)
{
char buf[PATH_MAX];
TEST(argc == 1, "argc should be 1\n");
TEST(argv[0] != 0, "argv[0] should not be NULL\n");
TEST(argv[1] == 0, "argv[1] should be NULL\n");
TEST(argv[0][0] != 0, "argv[0] should not be empty\n");
TEST(snprintf(buf, sizeof buf, "%s", argv[0]) < sizeof buf, "argv[0] is not a valid path\n");
return t_status;
}
这个程序就是检查命令行的参数是正确。如果参数正确,就悄无声息地结束。如果不正确,就会显示 "failed: ....",我们的测试脚本就会发现。但我们仔细看了QEMU上的整个执行过程,发现问题现象直接体现在zCore内核的内存管理上。首先,测试脚本生成的
rboot.conf中,采用了最详细的
trace`级别的log记录,希望能够得到详细的出错信息:
# rboot.conf
cmdline=LOG=trace: ......
另外,采用了另外一个脚本scripts\baremetal-libc-test-ones.py
来根据scripts\linux\baremetal-test-ones
的内容来测试个别的测试用例。这里就测argv-static.exe
和argv.exe
,并对结果进行分析和对比。argv-static.exe
的测试结果出现了如下的错误信息:
# 测试`argv-static.exe`的命令。其中的rboot.conf中包含的是"argv-static.exe"字符串
cp rboot.conf target/x86_64/release/esp/EFI/Boot/rboot.conf
timeout --foreground 8s qemu-system-x86_64 -machine q35 -cpu Haswell,+smap,-check,-fsgsbase -drive if=pflash,format=raw,readonly,file=../rboot/OVMF.fd -drive format=raw,file=fat:rw:target/x86_64/release/esp -device ich9-ahci,id=ahci -serial mon:stdio -m 4G -nic none -device isa-debug-exit,iobase=0xf4,iosize=0x04 -display none -nographic
## rboot输出
...
INFO: cmdline: "LOG=trace:TERM=xterm-256color:console.shell=true:virtcon.disable=true:ROOTPROC=/libc-test/src/functional/argv-static.exe?",
...
## zCore的log输出
...
## 重点:这是zCore内核传递给argv-static.exe应用程序的auxv变量数组
[3.21152631s DEBUG 0 0:0] ProcInitInfo auxv: {
0x3: 0x40,
0x4: 0x38,
0x5: 0x6,
0x6: 0x1000,
0x7: 0x0,
0x9: 0x40102f,
}
entry:0x40102f, sp:0x40fef0
...
## 重点:出现了内存页访问异常,导致内核panic了
[3.21898016s ERROR 0 0:0] page fualt from user mode 0x40 READ
[3.219374289s ERROR 0 0:0]
panicked at 'Page Fault from user mode UserContext {
general: GeneralRegs {
rax: 0x40,
rbx: 0x0,
rcx: 0x6,
rdx: 0x407050,
rsi: 0x0,
rdi: 0x38,
rbp: 0x800000,
rsp: 0x40fd50,
r8: 0x0,
r9: 0x20000,
r10: 0x0,
r11: 0x40,
r12: 0x40fef8,
r13: 0x401139,
r14: 0x0,
r15: 0x0,
rip: 0x401c25,
rflags: 0x3246,
fsbase: 0x0,
gsbase: 0x0,
},
trap_num: 0xe,
error_code: 0x4,
}', /media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/zCore/linux-loader/src/lib.rs:112:25
首先,我们需要分析一下内核panic时的情况,zCore详细列出了由于用户态应用程序argv-static.exe
执行时访问了某个地址,产生了了严重的page fault
异常时的寄存器信息。我们需要关注的是:
- rip: 0x401c25:这是
argv-static.exe
产生page fault
异常时的指令地址
有了这个信息,我们就可以分析一下,argv-static.exe
程序在执行到哪里出现的page fault
异常。光看argv-static.exe
程序的源码,完全没有啥问题。但我们知道argv.c
程序被gcc
编译器编译时,插入了不少库文件,才形成的argv-static.exe
可执行程序。我们反汇编一下argv-static.exe
可执行程序:
$ objdump -S argv-static.exe
...
0000000000401bd0 <__init_tls>:
401bd0: f3 0f 1e fa endbr64
401bd4: 55 push %rbp
401bd5: 53 push %rbx
401bd6: 48 83 ec 08 sub $0x8,%rsp
401bda: 48 8b 4f 28 mov 0x28(%rdi),%rcx
401bde: 4c 8b 5f 18 mov 0x18(%rdi),%r11
401be2: 48 85 c9 test %rcx,%rcx
401be5: 0f 84 a5 01 00 00 je 401d90 <__init_tls+0x1c0>
401beb: 44 8b 0d 32 54 00 00 mov 0x5432(%rip),%r9d # 407024 <__default_stacksize>
401bf2: 48 8b 7f 20 mov 0x20(%rdi),%rdi
401bf6: 4c 89 d8 mov %r11,%rax
401bf9: 31 db xor %ebx,%ebx
401bfb: 31 f6 xor %esi,%esi
401bfd: 45 31 c0 xor %r8d,%r8d
401c00: bd 00 00 80 00 mov $0x800000,%ebp
401c05: eb 1e jmp 401c25 <__init_tls+0x55>
401c07: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
401c0e: 00 00
401c10: 83 fa 07 cmp $0x7,%edx
401c13: 0f 85 3b 01 00 00 jne 401d54 <__init_tls+0x184>
401c19: 49 89 c0 mov %rax,%r8
401c1c: 48 01 f8 add %rdi,%rax
401c1f: 48 83 e9 01 sub $0x1,%rcx
401c23: 74 2c je 401c51 <__init_tls+0x81>
401c25: 8b 10 mov (%rax),%edx
...
我们终于看到了出错的代码401c25: 8b 10 mov (%rax),%edx
。这条指令的含义是把%rax
寄存器中的内容作为内存地址,把此内存地址中的内容赋值给%edx
。所以,出错的原因可以比较清晰地理解为rax
的值为0x40
,而这个0x40
这个地址是一个非法地址,所以当访问这个地址时,就出现了page fault
异常。
为何会出现非法地址呢?这应该是给rax
赋值搞错了。我们需要进一步 了解这段汇编是要干啥,这就需要了解__init_tls
这个musl libc的C函数的具体内容了。通过字符串搜索工具ag
对musl libc 1.1.24 (编译argv-static.exe用到的musl libc版本)进行字符串搜索,可以定位到这个函数在src/env/__init_tls.c
这个文件中的static_init_tls
函数:
static void static_init_tls(size_t *aux)
{
unsigned char *p;
size_t n;
Phdr *phdr, *tls_phdr=0;
size_t base = 0;
void *mem;
for (p=(void *)aux[AT_PHDR],n=aux[AT_PHNUM]; n; n--,p+=aux[AT_PHENT]) {
phdr = (void *)p;
if (phdr->p_type == PT_PHDR)
base = aux[AT_PHDR] - phdr->p_vaddr;
if (phdr->p_type == PT_DYNAMIC && _DYNAMIC)
base = (size_t)_DYNAMIC - phdr->p_vaddr;
if (phdr->p_type == PT_TLS)
tls_phdr = phdr;
if (phdr->p_type == PT_GNU_STACK &&
phdr->p_memsz > __default_stacksize)
__default_stacksize =
phdr->p_memsz < DEFAULT_STACK_MAX ?
phdr->p_memsz : DEFAULT_STACK_MAX;
}
......
}
weak_alias(static_init_tls, __init_tls); //这说明汇编中的`__init_tls`函数就是`static_init_tls`函数
在这个函数中,我们看到了对aux的读操作。调用static_init_tls
函数的函数在哪?再用ag
工具帮忙搜索,可以在src/env/__libc_start_main.c
文件中的__init_libc
函数中发现对__init_tls
函数的调用。在进一步分析__init_libc
函数,可以看到aux
其实就是zCore操作系统要传给应用程序的环境变量Auxiliary Vector
。这里有很多变量或定义的名称我也不记得是啥了,网络搜索一下,发现在:
- OpenPOWER 64-Bit ELF V2 ABI Specification 中介绍了Auxiliary Vector,以及AT_PHDR等的规范与含义。
虽然是OpenPOWER,具体含义套用到x86-64上好像完全没有问题。
看来可能是内核给aux的值给错了。但我们还缺少进一步的证据。
由于argv-static.exe
其实是一个Linux可执行程序,我们可以看看它在Linux上的执行情况:
## in ubuntu-20.04 x86_64
$ cd zCore/rootfs/libc-test/src/functional
$ ./argv-static.exe
没有任何输出,悄无声息地正常结束了。这看来还不行,我们需要更详细的分析,于是gdb
就登场了。通过gdb,我们可以在汇编级调试应用程序:
## in ubuntu-20.04 x86_64
$ cd zCore/rootfs/libc-test/src/functional
$ gdb argv-static.exe
(gdb) disassemble 0x401c25 ##看看能否找到出错的指令
Dump of assembler code for function __init_tls:
0x0000000000401bd0 <+0>: endbr64
0x0000000000401bd4 <+4>: push %rbp
......
0x0000000000401c25 <+85>: mov (%rax),%edx
......
(gdb) break __init_tls ## 在__init_tls函数入口设置断点
Breakpoint 1 at 0x401bd0
(gdb) run ##执行argv-static.exe
Breakpoint 1, 0x0000000000401bd0 in __init_tls ()
(gdb) si ##单步执行汇编
0x0000000000401bd4 in __init_tls ()
(gdb) i r ##显示寄存器内容
rax 0x0 0
rbx 0x0 0
rcx 0xbfebfbff 3219913727
......
## 为了观察正常执行到0x401c25处的寄存器情况,我们重复上面两个步骤
## 发现当执行0x401c25处指令是正常的,且此时的寄存器rax内容为:
(gdb) i r ##显示寄存器内容
rax 0x400040 4194368
## 注意!!! 这里的rax应该是0x400040
这样,我们就比较确定,在zCore中确实给rax赋值错了,应该是0x400040
,而不是0x40
。这两个值之间差了0x400000
。通过mus libc
的static_init_tls
函数,我们大致知道是访问aux数组(即Auxiliary Vector)的内容出了问题。我们需要看看zCore内核做了啥。
首先,我们需要找到zCore在哪里给应用程序传递Auxiliary Vector的。再次通过强大的ag
字符串搜索工具查找zCore中的文件:
$ cd zCore
$ ag AT_PHDR
linux-object/src/loader/abi.rs
113:pub const AT_PHDR: u8 = 3;
linux-object/src/loader/mod.rs
76: map.insert(abi::AT_PHDR, base + elf.header.pt2.ph_offset() as usize);
81: map.insert(abi::AT_PHDR, phdr_vaddr as usize);
我们需要对linux-object/src/loader/mod.rs
进行进一步分析。上述代码位于LinuxElfLoader::load
函数中。在此函数中,完成了对Linux执行程序(动态执行程序或静态执行程序)的ELF格式分析;内存空间分配;把文件中的代码和数据拷贝到内存空间中;针对动态执行程序还需完成代码/数据的重定位;分配应用程序的用户栈等。注意:最后还要把应用程序启动时需要的argc
、argv
、envionment
、auxiliary vector
拷贝都应用程序的用户栈中:
pub fn load( ... ) -> LxResult<(VirtAddr, VirtAddr)> {
......
let info = abi::ProcInitInfo {
args,
envs,
auxv: {
let mut map = BTreeMap::new();
#[cfg(target_arch = "x86_64")]
{
map.insert(abi::AT_BASE, base);
map.insert(abi::AT_PHDR, base + elf.header.pt2.ph_offset() as usize);
map.insert(abi::AT_ENTRY, entry);
}
#[cfg(target_arch = "riscv64")]
if let Some(phdr_vaddr) = elf.get_phdr_vaddr() {
map.insert(abi::AT_PHDR, phdr_vaddr as usize);
}
map.insert(abi::AT_PHENT, elf.header.pt2.ph_entry_size() as usize);
map.insert(abi::AT_PHNUM, elf.header.pt2.ph_count() as usize);
map.insert(abi::AT_PAGESZ, PAGE_SIZE);
map
},
};
let init_stack = info.push_at(sp);
stack_vmo.write(self.stack_pages * PAGE_SIZE - init_stack.len(), &init_stack)?;
sp -= init_stack.len();
debug!(
"ProcInitInfo auxv: {:#x?}\nentry:{:#x}, sp:{:#x}",
info.auxv, entry, sp
);
Ok((entry, sp))
从上面的代码,我们可以看到zCore对argc
、argv
、envionment
、auxiliary vector
拷贝都应用程序的用户栈的具体执行过程。特别是debug!
宏输出了发给应用程序的aux
信息。这就是最开始我们看到的出错内核输出内容:
## 重点:这是zCore内核传递给argv-static.exe应用程序的auxv变量数组
[3.21152631s DEBUG 0 0:0] ProcInitInfo auxv: {
0x3: 0x40, // #define AT_PHDR 3 /* Program headers for program */
0x4: 0x38,
0x5: 0x6,
0x6: 0x1000,
0x7: 0x0,
0x9: 0x40102f,
}
entry:0x40102f, sp:0x40fef0
...
## 重点:出现了内存页访问异常,导致内核panic了
[3.21898016s ERROR 0 0:0] page fualt from user mode 0x40 READ
[3.219374289s ERROR 0 0:0]
panicked at 'Page Fault from user mode UserContext {
general: GeneralRegs {
rax: 0x40, // aux[AT_PHDR]=0x40
rbx: 0x0,
rcx: 0x6,
rdx: 0x407050,
rsi: 0x0,
rdi: 0x38,
rbp: 0x800000,
rsp: 0x40fd50,
r8: 0x0,
r9: 0x20000,
r10: 0x0,
r11: 0x40,
r12: 0x40fef8,
r13: 0x401139,
r14: 0x0,
r15: 0x0,
rip: 0x401c25,
rflags: 0x3246,
fsbase: 0x0,
gsbase: 0x0,
},
trap_num: 0xe,
error_code: 0x4,
}', /media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/zCore/linux-loader/src/lib.rs:112:25
注意0x3: 0x40,
和ax: 0x40,
这两行。并结合之前的static_init_tls
函数的代码内容,可以在到:
//`static_init_tls`函数
p=(void *)aux[AT_PHDR] //aux[AT_PHDR]=0x40 所以访问0x40地址就出现了异常
//auxv.h
#define AT_PHDR 3 /* Program headers for program */
这就对上了,即zCore给aux[AT_PHDR]错误赋值为0x40后,导致了应用程序访问0x40这个非法地址,出现了page fault异常。
知道了zCore出错原因后,我们的工作就到了最后阶段:修补bug。首先,根据我们前面通过gdb调试argv-static.exe
在linux上的正确执行过程,可以知道aux[AT_PHDR]的正确值应该是0x400040
。而这个地址的含义是Program headers for program
,即ELF执行文件的Program Headers
。操作系统会把ELF执行文件中的Program Headers
拷贝到执行文件对应进程的内存中,如下图的的Headers
位置所示:
参考自OpenPOWER 64-Bit ELF V2 ABI Specification中的Figure 4.1. File Image to Process Memory Image Mapping
通过readelf
工具,我们可以看到ELF执行文件argv-static.exe
的Program Headers
信息:
$ readelf -l argv-static.exe
Elf 文件类型为 EXEC (可执行文件)
Entry point 0x40102f
There are 6 program headers, starting at offset 64
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000190 0x0000000000000190 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000003ee7 0x0000000000003ee7 R E 0x1000
LOAD 0x0000000000005000 0x0000000000405000 0x0000000000405000
0x0000000000000d1c 0x0000000000000d1c R 0x1000
LOAD 0x0000000000005fe8 0x0000000000406fe8 0x0000000000406fe8
0x0000000000000040 0x00000000000002f0 RW 0x1000
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000005fe8 0x0000000000406fe8 0x0000000000406fe8
0x0000000000000018 0x0000000000000018 R 0x1
Section to Segment mapping:
段节...
00
01 .init .text .fini
02 .rodata .eh_frame
03 .init_array .fini_array .got .got.plt .data .bss
04
05 .init_array .fini_array .got
这里我们看到了第一个program header
的虚拟地址为0x400000
,参数为0x190
。这个区域就是包含了aux[AT_PHDR]
的值0x400040
指向的Program Headers
区的进程内存区域。现在再仔细分析LinuxElfLoader::load
函数:
let base = image_vmar.addr(); //base = 0 ,而正确的值应该是0x400000
...
map.insert(abi::AT_PHDR, base + elf.header.pt2.ph_offset() as usize); //elf.header.pt2.ph_offset() 的返回值为0x40
这就清楚了我们需要修改地方,即需要判断当前程序是否是静态仔细程序,如果是,我们需要把base设置为第一个Program Header
的VirtAddr
起始地址。于是,我们进行了如下的修改:
......
//获得静态执行程序第一个Program Header's VirtAddr
let ph: ProgramHeader = elf.program_iter().next().unwrap();
let static_prog_base = ph.virtual_addr() as usize / PAGE_SIZE * PAGE_SIZE;
......
match elf.relocate(base) {
Ok(()) => info!("elf relocate passed !"),
Err(error) => { //表示这是静态执行程序,
base = static_prog_base; //需要修正base值为第一个Program Header's VirtAddr
warn!("elf relocate Err:{:?}, base {:x?}", error, base);
}
}
这样,增加了3行代码,程序就修改完毕了。再次执行测试用例,可以看到argv-static.exe
可以在运行在QEMU上的内核态zCore上正常执行了。