Skip to content

fix zcore bug

Yu Chen edited this page Aug 10, 2021 · 4 revisions

一次发现并修补(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支持测试单个测例

基本思路

于是我们开始进行对zCore的CI/CD进行扩展。实现内核态zCore的测试,需要有一个模拟物理机的虚拟机软件(我们采用的QEMU)。参考zCore现有Makefile中的一些运行脚本,我们首先需要让zCore通过某种方式很方便地支持启动不同的应用程序。这其实在zCore中已经有所考虑了。以x86-64的硬件环境为例,zCore支持基于UEFIbootloaderrboot的启动,而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的CI/CD脚本

在已有.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中文件系统对文件的读写准备的,目前其实没有用到。这里面的大致执行流程的含义写在了usesname中。具体的步骤是:

  1. runs-on: ubuntu-20.04:在ubuntu-20.04 x86-64上运行测试
  2. uses: actions/checkout@v2:获取zCore repo
  3. uses: actions-rs/toolchain@v1:安装rustc-nightly-2021-07-27工具链,包含rust-src component
  4. name: Pull prebuilt images:下载预编译的fs镜像,和用于LibOS模式的定制libc-libos.so (这里的测试其实用不上)
  5. name: Install musl toolchain qemu-system-x86:安装编译musl libc-tests测试用例的工具和QEMU工具
  6. name: Prepare rootfs and libc-test:构建rootfs,编译libc-tests测试用例,并放到rootfs中
  7. name: Build kernel:编译zCore kernel
  8. name: create qemu disk:建立一个虚拟盘,用于文件相关的测试(目前的测试没有用)
  9. name: Run baremetal-libc-test:执行测试脚本,测试libc-test测试用例

这样,就建立好了基于GITHUB的CI/CD自动测试。

发现zCore bug

把bug的范围缩小到最小

通过对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.exeargv.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,具体含义套用到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 libcstatic_init_tls函数,我们大致知道是访问aux数组(即Auxiliary Vector)的内容出了问题。我们需要看看zCore内核做了啥。

分析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格式分析;内存空间分配;把文件中的代码和数据拷贝到内存空间中;针对动态执行程序还需完成代码/数据的重定位;分配应用程序的用户栈等。注意:最后还要把应用程序启动时需要的argcargvenvionmentauxiliary 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对argcargvenvionmentauxiliary 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

知道了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

  • avatar

通过readelf工具,我们可以看到ELF执行文件argv-static.exeProgram 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 HeaderVirtAddr起始地址。于是,我们进行了如下的修改:

......
//获得静态执行程序第一个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上正常执行了。