diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml new file mode 100644 index 00000000..823d0470 --- /dev/null +++ b/.github/workflows/github-pages.yml @@ -0,0 +1,64 @@ +name: GitHub Pages + +on: + push: + sources: + - "Pages/**" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + pages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install mdbook and its preprocessors + run: | + mkdir mdbook + tag=$(curl 'https://api.github.com/repos/toolmanp/rs-mdbook-callouts/releases/latest' | jq -r '.tag_name') + url="https://github.com/toolmanp/rs-mdbook-callouts/releases/download/${tag}/mdbook-callouts-x86_64-unknown-linux-gnu.tar.gz" + curl -sSL $url | tar -xz --directory=./mdbook + tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') + url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" + curl -sSL $url | tar -xz --directory=./mdbook + tag=$(curl 'https://api.github.com/repos/badboy/mdbook-toc/releases/latest' | jq -r '.tag_name') + url="https://github.com/badboy/mdbook-toc/releases/download/${tag}/mdbook-toc-${tag}-x86_64-unknown-linux-gnu.tar.gz" + curl -sSL $url | tar -xz --directory=./mdbook + tag=$(curl 'https://api.github.com/repos/badboy/mdbook-last-changed/releases/latest' | jq -r '.tag_name') + url="https://github.com/badboy/mdbook-last-changed/releases/download/${tag}/mdbook-last-changed-${tag}-x86_64-unknown-linux-gnu.tar.gz" + curl -sSL $url | tar -xz --directory=./mdbook + tag=$(curl 'https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest' | jq -r '.tag_name') + url="https://github.com/badboy/mdbook-mermaid/releases/download/${tag}/mdbook-mermaid-${tag}-x86_64-unknown-linux-gnu.tar.gz" + curl -sSL $url | tar -xz --directory=./mdbook + echo `pwd`/mdbook >> $GITHUB_PATH + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Build Book + run: | + mdbook-mermaid install ./ + mdbook build + touch ./book/.nojekyll + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'book' + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index ed1fedfc..858e1e69 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ infer_*_report.txt infer-out/ .repo/ *.tar.gz +book/ +**mermaid** +**/index.html diff --git a/os-book.jpeg b/Assets/os-book.jpeg similarity index 100% rename from os-book.jpeg rename to Assets/os-book.jpeg diff --git a/Lab0/Makefile b/Lab0/Makefile index 24b940af..53e00d28 100644 --- a/Lab0/Makefile +++ b/Lab0/Makefile @@ -1,16 +1,15 @@ LAB := 0 -ifndef PROJECT -PROJECT := $(shell git rev-parse --show-toplevel) -endif +PROJECT := $(CURDIR)/.. +GRADER := $(PROJECT)/Scripts/grader.sh -export CURDIR LAB +export PROJECT CURDIR LAB -include $(PROJECT)/scripts/lab.mk +include $(PROJECT)/Scripts/lab.mk bomb: student-number.txt @echo " GEN bomb" - @$(CURDIR)/scripts/generate_bomb.sh + @$(CURDIR)/Scripts/generate_bomb.sh clean: @echo " CLEAN bomb" @@ -30,7 +29,7 @@ gdb: $(GDB) -ex "set architecture aarch64" -ex "target remote localhost:1234" -ex "add-symbol-file bomb" grade: - @$(CURDIR)/scripts/grade/lab0.sh + @$(GRADER) submit: @echo " SUBMIT lab0" diff --git a/Lab0/README.md b/Lab0/README.md deleted file mode 100644 index 31358f77..00000000 --- a/Lab0/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Lab0 拆炸弹 - -> [!WARNING] -> 在完成本实验之前,请务必将你的学号填写在student-number.txt当中,否则本lab实验的成绩将计为0分 - -## Makefile 讲解 - -- `make bomb`: 使用student-number.txt提供的学号,生成炸弹,如果您不是上海交通大学的学生可以自行随意填写。 -- `make qemu`: 使用qemu-aarch64二进制模拟运行炸弹 -- `make qemu-gdb`: 使用qemu-aarch64提供的gdb server进行调试 -- `make gdb`: 使用仓库目录自动生成的$(GDB)定义连接到qemu-aarch64的gdb-server进行调试 - -## 教程 - -> [!TIP] -> 请阅读[tools-tutorial.pdf](docs/tools-tutorial.pdf)了解如何使用调试工具 -> 请仔细阅读[instruction.pdf](docs/instruction.pdf)了解如何完成本实验 - -## 评分与提交规则 - -> [!IMPORTANT] -> 运行 `make grade` 来得到本实验的分数 -> 运行 `make submit` 会在检查student-number.txt内容之后打包必要的提交文件 - -## 附录 - -- 线上教程[https://www.bilibili.com/video/BV1q94y1a7BF/?vd_source=63231f40c83c4d292b2a881fda478960] diff --git a/Lab0/docs/instructions.pdf b/Lab0/docs/instructions.pdf deleted file mode 100644 index f0d9209c..00000000 Binary files a/Lab0/docs/instructions.pdf and /dev/null differ diff --git a/Lab0/docs/tools-tutorial.pdf b/Lab0/docs/tools-tutorial.pdf deleted file mode 100644 index e5dfd794..00000000 Binary files a/Lab0/docs/tools-tutorial.pdf and /dev/null differ diff --git a/Lab0/scripts/grade/expects/lab0.exp b/Lab0/grade.exp similarity index 100% rename from Lab0/scripts/grade/expects/lab0.exp rename to Lab0/grade.exp diff --git a/Lab0/scripts/generate_bomb.sh b/Lab0/scripts/generate_bomb.sh index e2428ed5..a78cf1ed 100755 --- a/Lab0/scripts/generate_bomb.sh +++ b/Lab0/scripts/generate_bomb.sh @@ -1,18 +1,23 @@ #!/bin/bash -if [ ! -d "${CURDIR}/warehouse" ]; then +if [[ -z $PROJECT ]]; then + echo "Please set the PROJECT environment variable to the root directory of your project. (Makefile)" + exit 1 +fi + +if [[ ! -d "${CURDIR}/warehouse" ]]; then echo "Please run under the root directory of this lab!" - exit -1 + exit 1 fi total_bombs=30 student=$(cat ${CURDIR}/student-number.txt) -if [ -z $student ]; then +if [[ -z $student ]]; then echo "Please enter your student number in student-number.txt at first! Otherwise you would recieve 0 POINTS!" - exit -1 + exit 1 fi -bomb_id=$(($student%$total_bombs+1)) +bomb_id=$(($student % $total_bombs + 1)) cp ${CURDIR}/warehouse/bomb-${bomb_id} ${CURDIR}/bomb diff --git a/Lab0/scripts/grade/lab0.sh b/Lab0/scripts/grade/lab0.sh deleted file mode 100755 index 7edfb975..00000000 --- a/Lab0/scripts/grade/lab0.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -PROJECT=$(git rev-parse --show-toplevel) -. $PROJECT/scripts/env_setup.sh - -make="${MAKE:-make}" - -grade_dir=$(dirname $0) - -info "Grading lab ${LAB} ...(may take 10 seconds)" - -$grade_dir/expects/lab0.exp -score=$? - -bold "===============" -info "Score: $score/100" diff --git a/Lab1/Makefile b/Lab1/Makefile index 4c9a1df0..6ad5e910 100644 --- a/Lab1/Makefile +++ b/Lab1/Makefile @@ -1,48 +1,2 @@ LAB := 1 - -V := @ -PROJECT_DIR := . -BUILD_DIR := $(PROJECT_DIR)/build -KERNEL_IMG := $(BUILD_DIR)/kernel.img -QEMU := qemu-system-aarch64 -_QEMU := $(PROJECT_DIR)/scripts/qemu/qemu_wrapper.sh $(QEMU) -QEMU_GDB_PORT := 1234 -QEMU_OPTS := -machine raspi3b -nographic -serial mon:stdio -m size=1G -kernel $(KERNEL_IMG) -GDB := gdb-multiarch -CHBUILD := $(PROJECT_DIR)/chbuild - -.PHONY: all -all: build - -.PHONY: defconfig build clean distclean - -defconfig: - $(V)$(CHBUILD) defconfig - -build: - $(V)test -f $(PROJECT_DIR)/.config || $(CHBUILD) defconfig - $(V)$(CHBUILD) build - -clean: - $(V)$(CHBUILD) clean - -distclean: - $(V)$(CHBUILD) distclean - -.PHONY: qemu qemu-gdb gdb - -qemu: - $(V)$(_QEMU) $(QEMU_OPTS) - -qemu-gdb: - $(V)$(_QEMU) -S -gdb tcp::$(QEMU_GDB_PORT) $(QEMU_OPTS) - -gdb: - $(V)$(GDB) --nx -x $(PROJECT_DIR)/.gdbinit - -.PHONY: grade - -grade: - $(V)test -f $(PROJECT_DIR)/.config && cp $(PROJECT_DIR)/.config $(PROJECT_DIR)/.config.bak - $(V)$(PROJECT_DIR)/scripts/grade/lab$(LAB).sh - $(V)test -f $(PROJECT_DIR)/.config.bak && mv $(PROJECT_DIR)/.config.bak $(PROJECT_DIR)/.config +include $(CURDIR)/../Scripts/lab.mk diff --git a/Lab1/README.md b/Lab1/README.md deleted file mode 100644 index 43b9ecc3..00000000 --- a/Lab1/README.md +++ /dev/null @@ -1,297 +0,0 @@ -This lab explains how to set the CPU exception level, configure the kernel page table, and enable MMU during kernel boot. -We will use a basic version of [ChCore microkernel](https://www.usenix.org/conference/atc20/presentation/gu) in the series of kernel labs and the experiment machine is Raspi3b+ (both QEMU and board are OK). - -Tutorial: [https://www.bilibili.com/video/BV1gj411i7dh/](https://www.bilibili.com/video/BV1gj411i7dh/?spm_id_from=333.337.search-card.all.click) - - -# 实验 1:机器启动 - -本实验作为 ChCore 操作系统课程实验的第一个实验,分为两个部分:第一部分介绍实验所需的基础知识,第二部分熟悉并完成ChCore 内核的启动过程。 -实验面向的硬件平台是树莓派3b+(AArch64)。你可以在QEMU模拟器上完成实验,也可以在树莓派开发板上完成。 - -实验中的“思考题”,请在实验报告中用文字或示意图简述,“练习题”则需在 ChCore 代码中填空,并在实验报告阐述实现过程,“挑战题”为难度稍高的练习题,此后的实验也类似,不再重复说明。 - -本实验代码包含了基础的ChCore 微内核操作系统,除了练习题相关的源码以外,其余部分通过二进制格式提供。 -完成本实验的练习题之后,你可以进入 ChCore shell,运行命令或执行程序。 -例如,可以在 shell 中输入 `hello_world.bin` 运行一个简单的用户态程序; -输入`ls` 查看目录内容。 - -``` - ______ __ __ ______ __ __ ______ __ __ -/\ ___\ /\ \_\ \ /\ ___\ /\ \_\ \ /\ ___\ /\ \ /\ \ -\ \ \____ \ \ __ \ \ \___ \ \ \ __ \ \ \ __\ \ \ \____ \ \ \____ - \ \_____\ \ \_\ \_\ \/\_____\ \ \_\ \_\ \ \_____\ \ \_____\ \ \_____\ - \/_____/ \/_/\/_/ \/_____/ \/_/\/_/ \/_____/ \/_____/ \/_____/ - - -Welcome to ChCore shell! -$ -``` - -## 第一部分:基础知识 - -### 构建系统 - -在 ChCore 根目录运行下面命令可以构建并运行 ChCore: - -```sh -$ make build # 构建 ChCore -$ make qemu # 使用 QEMU 运行 ChCore(初始时运行不会有任何输出),按 Ctrl-A 再按 X 退出 -``` - -小贴士: - -ChCore 采用 CMake 编写的构建系统管理其构建过程,并通过 Shell 脚本 `./chbuild` 对 CMake 的配置(configure)、构建(build)和清理(clean)的操作进行管理,另外,为了同学们更方便地进行实验,在 ChCore 实验中又添加了 Makefile 进一步对 `./chbuild` 脚本进行封装。 - -具体地,在根目录的 `CMakeLists.txt` 中,通过 `chcore_add_subproject` 命令(实际上就是 CMake 内置的 [`ExternalProject_Add`](https://cmake.org/cmake/help/latest/module/ExternalProject.html))添加了 kernel 子项目,并传入根级 `CMakeLists.txt` 在 configure 步骤后获得的配置(CMake cache 变量);在 kernel 子项目中,通过各级子目录共同构建了 `kernel.img` 文件,并放在 ChCore 根目录的 `build` 目录下。 - -### AArch64 汇编 - -在课程中,我们已经介绍过和实验相关的AArch64汇编知识,系列实验中的Bomb-Lab也是帮助大家熟悉AArch64汇编。下面提供了一些资料可供查阅。 - -AArch64 是 ARMv8 ISA 的 64 位执行状态。在 ChCore 实验中需要理解 AArch64 架构的一些特性,并能看懂和填写 AArch64 汇编代码,因此请先参考 [Arm Instruction Set Reference Guide](https://developer.arm.com/documentation/100076/0100) 的 [Overview of the Arm Architecture](https://developer.arm.com/documentation/100076/0100/instruction-set-overview/overview-of-the-arm-architecture) 和 [Overview of AArch64 state](https://developer.arm.com/documentation/100076/0100/instruction-set-overview/overview-of-aarch64-state) 章节以对 AArch64 架构有基本的认识,[A64 Instruction Set Reference](https://developer.arm.com/documentation/100076/0100/a64-instruction-set-reference) 章节则是完整的指令参考手册。 - -除此之外,可以阅读 [A Guide to ARM64 / AArch64 Assembly on Linux with Shellcodes and Cryptography](https://modexp.wordpress.com/2018/10/30/arm64-assembly/) 的第 1、2 部分,以快速熟悉 AArch64 汇编。 - -### QEMU 和 GDB - -在完成ChCore实验的过程中,我们常常需要对 ChCore 内核和用户态代码进行调试,因此需要开启 QEMU 的 GDB server,并使用 GDB(在 x86-64 平台的 Ubuntu 系统上应使用 `gdb-multiarch` 命令)连接远程目标(运行ChCore的虚拟机)来进行调试。 - -ChCore 根目录提供了 `.gdbinit` 文件来对 GDB 进行初始化,以方便使用。 - -要使用 GDB 调试 ChCore,需打开两个终端页面,并在 ChCore 根目录分别依次运行: - -```sh -# 终端 1 -$ make qemu-gdb # 需要先确保已运行 make build - -# 终端 2 -$ make gdb -``` - -不出意外的话,终端 1 将会“卡住”没有任何输出(QEMU 使用 Ctrl-A + X 退出),终端 2 将会进入 GDB 调试界面,并显示从 `0x80000` (这是树莓派3b+平台指定的内核第一行指令地址)内存地址开始的指令。此时在 GDB 窗口输入命令可以控制 QEMU 中 ChCore 内核的运行,例如: - -- `ni` 可以执行到下一条指令 -- `si` 可以执行到下一条指令,且会跟随 `bl` 进入函数 -- `break [func]`/`b [func]` 可以在函数 `[func]` 开头打断点 -- `break *[addr]` 可以在内存地址 `[addr]` 处打断点 -- `info reg [reg]`/`i r [reg]` 可以打印 `[reg]` 寄存器的值 -- `continue`/`c` 可以继续 ChCore 的执行,直到触发断点或手动按 Ctrl-C - -更多常用的 GDB 命令和用法请参考 [GDB Quick Reference](https://users.ece.utexas.edu/~adnan/gdb-refcard.pdf) 和 [Debugging with GDB](https://sourceware.org/gdb/onlinedocs/gdb/)。 - -## 第二部分:内核启动过程 - -### 树莓派启动过程 - -在树莓派 3B+ 真机上,通过 SD 卡启动时,上电后会运行 ROM 中的特定固件,接着加载并运行 SD 卡上的 `bootcode.bin` 和 `start.elf`,后者进而根据 `config.txt` 中的配置,加载指定的 kernel 映像文件(纯 binary 格式,通常名为 `kernel8.img`)到内存的 `0x80000` 位置并跳转到该地址开始执行。 - -而在 QEMU 模拟的 `raspi3b`(旧版 QEMU 为 `raspi3`)机器上,则可以通过 `-kernel` 参数直接指定 ELF 格式的 kernel 映像文件,进而直接启动到 ELF 头部中指定的入口地址,即 `_start` 函数(实际上也在 `0x80000`,因为 ChCore 通过 linker script 强制指定了该函数在 ELF 中的位置,如有兴趣请参考附录)。 - -### 启动 CPU 0 号核 - -`_start` 函数(位于 `kernel/arch/aarch64/boot/raspi3/init/start.S`)是 ChCore 内核启动时执行的第一块代码。由于 QEMU 在模拟机器启动时会同时开启 4 个 CPU 核心,于是 4 个核会同时开始执行 `_start` 函数。而在内核的初始化过程中,我们通常需要首先让其中一个核进入初始化流程,待进行了一些基本的初始化后,再让其他核继续执行。 - -> 思考题 1:阅读 `_start` 函数的开头,尝试说明 ChCore 是如何让其中一个核首先进入初始化流程,并让其他核暂停执行的。 -> -> 提示:可以在 [Arm Architecture Reference Manual](https://documentation-service.arm.com/static/61fbe8f4fa8173727a1b734e) 找到 `mpidr_el1` 等系统寄存器的详细信息。 - -### 切换异常级别 - -AArch64 架构中,特权级被称为异常级别(Exception Level,EL),四个异常级别分别为 EL0、EL1、EL2、EL3,其中 EL3 为最高异常级别,常用于安全监控器(Secure Monitor),EL2 其次,常用于虚拟机监控器(Hypervisor),EL1 是内核常用的异常级别,也就是通常所说的内核态,EL0 是最低异常级别,也就是通常所说的用户态。 - -QEMU `raspi3b` 机器启动时,CPU 异常级别为 EL3,我们需要在启动代码中将异常级别降为 EL1,也就是进入内核态。具体地,这件事是在 `arm64_elX_to_el1` 函数(位于 `kernel/arch/aarch64/boot/raspi3/init/tools.S`)中完成的。 - -为了使 `arm64_elX_to_el1` 函数具有通用性,我们没有直接写死从 EL3 降至 EL1 的逻辑,而是首先判断当前所在的异常级别,并根据当前异常级别的不同,跳转到相应的代码执行。 - -> 练习题 2:在 `arm64_elX_to_el1` 函数的 `LAB 1 TODO 1` 处填写一行汇编代码,获取 CPU 当前异常级别。 -> -> 提示:通过 `CurrentEL` 系统寄存器可获得当前异常级别。通过 GDB 在指令级别单步调试可验证实现是否正确。注意参考文档理解 `CurrentEL` 各个 bits 的[意义](https://developer.arm.com/documentation/ddi0601/2020-12/AArch64-Registers/CurrentEL--Current-Exception-Level)。 - -`eret`指令可用于从高异常级别跳到更低的异常级别,在执行它之前我们需要设置 -设置 `elr_elx`(异常链接寄存器)和 `spsr_elx`(保存的程序状态寄存器),分别控制`eret`执行后的指令地址(PC)和程序状态(包括异常返回后的异常级别)。 - -> 练习题 3:在 `arm64_elX_to_el1` 函数的 `LAB 1 TODO 2` 处填写大约 4 行汇编代码,设置从 EL3 跳转到 EL1 所需的 `elr_el3` 和 `spsr_el3` 寄存器值。 -> -> 提示:`elr_el3` 的正确设置应使得控制流在 `eret` 后从 `arm64_elX_to_el1` 返回到 `_start` 继续执行初始化。 `spsr_el3` 的正确设置应正确屏蔽 DAIF 四类中断,并且将 [SP](https://developer.arm.com/documentation/ddi0500/j/CHDDGJID) 正确设置为 `EL1h`. 在设置好这两个系统寄存器后,不需要立即 `eret`. - - -练习完成后,可使用 GDB 跟踪内核代码的执行过程,由于此时不会有任何输出,可通过是否正确从 `arm64_elX_to_el1` 函数返回到 `_start` 来判断代码的正确性。 - -### 跳转到第一行 C 代码 - -降低异常级别到 EL1 后,我们准备从汇编跳转到 C 代码,在此之前我们先设置栈(SP)。因此,`_start` 函数在执行 `arm64_elX_to_el1` 后,即设置内核启动阶段的栈,并跳转到第一个 C 函数 `init_c`。 - -> 思考题 4:说明为什么要在进入 C 函数之前设置启动栈。如果不设置,会发生什么? - -进入 `init_c` 函数后,第一件事首先通过 `clear_bss` 函数清零了 `.bss` 段,该段用于存储未初始化的全局变量和静态变量(具体请参考附录)。 - -> 思考题 5:在实验 1 中,其实不调用 `clear_bss` 也不影响内核的执行,请思考不清理 `.bss` 段在之后的何种情况下会导致内核无法工作。 - -### 初始化串口输出 - -到目前为止我们仍然只能通过 GDB 追踪内核的执行过程,而无法看到任何输出,这无疑是对我们写操作系统的积极性的一种打击。因此在 `init_c` 中,我们启用树莓派的 UART 串口,从而能够输出字符。 - -在 `kernel/arch/aarch64/boot/raspi3/peripherals/uart.c` 已经给出了 `early_uart_init` 和 `early_uart_send` 函数,分别用于初始化 UART 和发送单个字符(也就是输出字符)。 - -> 练习题 6:在 `kernel/arch/aarch64/boot/raspi3/peripherals/uart.c` 中 `LAB 1 TODO 3` 处实现通过 UART 输出字符串的逻辑。 - -恭喜!我们终于在内核中输出了第一个字符串! -感兴趣的同学请思考`early_uart_send`究竟是怎么输出字符的。 - -### 启用 MMU - -在内核的启动阶段,还需要配置启动页表(`init_kernel_pt` 函数),并启用 MMU(`el1_mmu_activate` 函数),使可以通过虚拟地址访问内存,从而为之后跳转到高地址作准备(内核通常运行在虚拟地址空间 `0xffffff0000000000` 之后的高地址)。 - -关于配置启动页表的内容由于包含关于页表的细节,将在本实验下一部分实现,目前直接启用 MMU。 - -在 EL1 异常级别启用 MMU 是通过配置系统寄存器 `sctlr_el1` 实现的(Arm Architecture Reference Manual D13.2.118)。具体需要配置的字段主要包括: - -- 是否启用 MMU(`M` 字段) -- 是否启用对齐检查(`A` `SA0` `SA` `nAA` 字段) -- 是否启用指令和数据缓存(`C` `I` 字段) - -> 练习题 7:在 `kernel/arch/aarch64/boot/raspi3/init/tools.S` 中 `LAB 1 TODO 4` 处填写一行汇编代码,以启用 MMU。 - -由于没有配置启动页表,在启用 MMU 后,内核会立即发生地址翻译错误(Translation Fault),进而尝试跳转到异常处理函数(Exception Handler), -该异常处理函数的地址为异常向量表基地址(`vbar_el1` 寄存器)加上 `0x200`。 -此时我们没有设置异常向量表(`vbar_el1` 寄存器的值是0),因此执行流会来到 `0x200` 地址,此处的代码为非法指令,会再次触发异常并跳转到 `0x200` 地址。 -使用 GDB 调试,在 GDB 中输入 `continue` 后,待内核输出停止后,按 Ctrl-C,可以观察到内核在 `0x200` 处无限循环。 - -## 第三部分:内核启动页表 - -### AArch64 地址翻译 - -在配置内核启动页表前,我们首先回顾实验涉及到的体系结构知识。这部分内容课堂上已经学习过,如果你已熟练掌握则可以直接跳过这里的介绍(但不要跳过思考题)。 - -在 AArch64 架构的 EL1 异常级别存在两个页表基址寄存器:`ttbr0_el1`[^ttbr0_el1] 和 `ttbr1_el1`[^ttbr1_el1],分别用作虚拟地址空间低地址和高地址的翻译。那么什么地址范围称为“低地址”,什么地址范围称为“高地址”呢?这由 `tcr_el1` 翻译控制寄存器[^tcr_el1]控制,该寄存器提供了丰富的可配置性,可决定 64 位虚拟地址的高多少位为 `0` 时,使用 `ttbr0_el1` 指向的页表进行翻译,高多少位为 `1` 时,使用 `ttbr1_el1` 指向的页表进行翻译[^ttbr-sel]。一般情况下,我们会将 `tcr_el1` 配置为高低地址各有 48 位的地址范围,即,`0x0000_0000_0000_0000`~`0x0000_ffff_ffff_ffff` 为低地址,`0xffff_0000_0000_0000`~`0xffff_ffff_ffff_ffff` 为高地址。 - -[^ttbr0_el1]: Arm Architecture Reference Manual, D13.2.144 -[^ttbr1_el1]: Arm Architecture Reference Manual, D13.2.147 -[^tcr_el1]: Arm Architecture Reference Manual, D13.2.131 -[^ttbr-sel]: Arm Architecture Reference Manual, D5.2 Figure D5-13 - -了解了如何决定使用 `ttbr0_el1` 还是 `ttbr1_el1` 指向的页表,再来看地址翻译过程如何进行。通常我们会将系统配置为使用 4KB 翻译粒度、4 级页表(L0 到 L3),同时在 L1 和 L2 页表中分别允许映射 2MB 和 1GB 大页(或称为块)[^huge-page],因此地址翻译的过程如下图所示: - -[^huge-page]: 操作系统:原理与实现 - -![](assets/lab1-trans.svg) - -其中,当映射为 1GB 块或 2MB 块时,图中 L2、L3 索引或 L3 索引的位置和低 12 位共同组成块内偏移。 - -每一级的每一个页表占用一个 4KB 物理页,称为页表页(Page Table Page),其中有 512 个条目,每个条目占 64 位。AArch64 中,页表条目称为描述符(descriptor)[^descriptor],最低位(bit[0])为 `1` 时,描述符有效,否则无效。有效描述符有两种类型,一种指向下一级页表(称为表描述符),另一种指向物理块(大页)或物理页(称为块描述符或页描述符)。在上面所说的地址翻译配置下,描述符结构如下(“Output address”在这里即物理地址,一些地方称为物理页帧号(Page Frame Number,PFN)): - -**L0、L1、L2 页表描述符** - -![](assets/lab1-pte-1.png) - -**L3 页表描述符** - -![](assets/lab1-pte-2.png) - -[^descriptor]: Arm Architecture Reference Manual, D5.3 - -> 思考题 8:请思考多级页表相比单级页表带来的优势和劣势(如果有的话),并计算在 AArch64 页表中分别以 4KB 粒度和 2MB 粒度映射 0~4GB 地址范围所需的物理内存大小(或页表页数量)。 - -页表描述符中除了包含下一级页表或物理页/块的地址,还包含对内存访问进行控制的属性(attribute)。这里涉及到太多细节,本文档限于篇幅只介绍最常用的几个页/块描述符中的属性字段: - -字段 | 位 | 描述 ---- | --- | --- -UXN | bit[54] | 置为 `1` 表示非特权态无法执行(Unprivileged eXecute-Never) -PXN | bit[53] | 置为 `1` 表示特权态无法执行(Privileged eXecute-Never) -nG | bit[11] | 置为 `1` 表示该描述符在 TLB 中的缓存只对当前 ASID 有效 -AF | bit[10] | 置为 `1` 表示该页/块在上一次 AF 置 `0` 后被访问过 -SH | bits[9:8] | 表示可共享属性[^mem-attr] -AP | bits[7:6] | 表示读写等数据访问权限[^mem-access] -AttrIndx | bits[4:2] | 表示内存属性索引,间接指向 `mair_el1` 寄存器中配置的属性[^mair_el1],用于控制将物理页映射为正常内存(normal memory)或设备内存(device memory),以及控制 cache 策略等 - -[^mem-attr]: Arm Architecture Reference Manual, D5.5 -[^mem-access]: Arm Architecture Reference Manual, D5.4 -[^mair_el1]: Arm Architecture Reference Manual, D13.2.97 - -### 配置内核启动页表 - -有了关于页表配置的前置知识,我们终于可以开始配置内核的启动页表了。 - -操作系统内核通常运行在虚拟内存的高地址(如前所述,`0xffff_0000_0000_0000` 之后的虚拟地址)。通过对内核页表的配置,将虚拟内存高地址映射到内核实际所在的物理内存,在执行内核代码时,PC 寄存器的值是高地址,对全局变量、栈等的访问都使用高地址。在内核运行时,除了需要访问内核代码和数据等,往往还需要能够对任意物理内存和外设内存(MMIO)进行读写,这种读写同样通过高地址进行。 - -因此,在内核启动时,首先需要对内核自身、其余可用物理内存和外设内存进行虚拟地址映射,最简单的映射方式是一对一的映射,即将虚拟地址 `0xffff_0000_0000_0000 + addr` 映射到 `addr`。需要注意的是,在 ChCore 实验中我们使用了 `0xffff_ff00_0000_0000` 作为内核虚拟地址的开始(注意开头 `f` 数量的区别),不过这不影响我们对知识点的理解。 - -在树莓派 3B+ 机器上,物理地址空间分布如下[^bcm2836]: - -[^bcm2836]: [bcm2836-peripherals.pdf](https://datasheets.raspberrypi.com/bcm2836/bcm2836-peripherals.pdf) & [Raspberry Pi Hardware - Peripheral Addresses](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#peripheral-addresses) - -物理地址范围 | 对应设备 ---- | --- -`0x00000000`~`0x3f000000` | 物理内存(SDRAM) -`0x3f000000`~`0x40000000` | 共享外设内存 -`0x40000000`~`0xffffffff` | 本地(每个 CPU 核独立)外设内存 - -现在将目光转移到 `kernel/arch/aarch64/boot/raspi3/init/mmu.c` 文件,我们需要在 `init_kernel_pt` 为内核配置从 `0x00000000` 到 `0x80000000`(`0x40000000` 后的 1G,ChCore 只需使用这部分地址中的本地外设)的映射,其中 `0x00000000` 到 `0x3f000000` 映射为 normal memory,`0x3f000000` 到 `0x80000000`映射为 device memory,其中 `0x00000000` 到 `0x40000000` 以 2MB 块粒度映射,`0x40000000` 到 `0x80000000` 以 1GB 块粒度映射。 - -> 思考题 9: 请结合上述地址翻译规则,计算在练习题 10 中,你需要映射几个 L2 页表条目,几个 L1 页表条目,几个 L0 页表条目。页表页需要占用多少物理内存? -> - -> 练习题 10:在 `init_kernel_pt` 函数的 `LAB 1 TODO 5` 处配置内核高地址页表(`boot_ttbr1_l0`、`boot_ttbr1_l1` 和 `boot_ttbr1_l2`),以 2MB 粒度映射。 -> -> 提示:你只需要将 `addr`(`0x00000000` 到 `0x80000000`) 按照要求的页粒度一一映射到 `KERNEL_VADDR + addr`(`vaddr`) 上。`vaddr` 对应的物理地址是 `vaddr - KERNEL_VADDR`. Attributes 的设置请参考给出的低地址页表配置。 - -> 思考题 11:请思考在 `init_kernel_pt` 函数中为什么还要为低地址配置页表,并尝试验证自己的解释。 - -完成 `init_kernel_pt` 函数后,ChCore 内核便可以在 `el1_mmu_activate` 中将 `boot_ttbr1_l0` 等物理地址写入实际寄存器(如 `ttbr1_el1` ),随后启用 MMU 后继续执行,并通过 `start_kernel` 跳转到高地址,进而跳转到内核的 `main` 函数(位于 `kernel/arch/aarch64/main.c`, 尚未发布,以 binary 提供)。 - -> 思考题 12:在一开始我们暂停了三个其他核心的执行,根据现有代码简要说明它们什么时候会恢复执行。思考为什么一开始只让 0 号核心执行初始化流程? -> -> 提示: `secondary_boot_flag` 将在 main 函数执行完时钟,调度器,锁的初始化后被设置。 - -## 附录 - -### ELF 文件格式 - -如第一部分所看到的,ChCore 的构建系统将会构建出 `build/kernel.img` 文件,该文件是一个 ELF 格式的“可执行目标文件”,和我们平常在 Linux 系统中见到的可执行文件如出一辙。ELF 可执行文件以 ELF 头部(ELF header)开始,后跟几个程序段(program segment),每个程序段都是一个连续的二进制块,其中又包含不同的分段(section),加载器(loader)将它们加载到指定地址的内存中并赋予指定的可读(R)、可写(W)、可执行(E)权限,并从入口地址(entry point)开始执行。 - -可以通过 `aarch64-linux-gnu-readelf` 命令查看 `build/kernel.img` 文件的 ELF 元信息(比如通过 `-h` 参数查看 ELF 头部、`-l` 参数查看程序头部、`-S` 参数查看分段头部等): - -```sh -$ aarch64-linux-gnu-readelf -h build/kernel.img -ELF Header: - Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 - Class: ELF64 - Data: 2's complement, little endian - Version: 1 (current) - OS/ABI: UNIX - System V - ABI Version: 0 - Type: EXEC (Executable file) - Machine: AArch64 - Version: 0x1 - Entry point address: 0x80000 - Start of program headers: 64 (bytes into file) - Start of section headers: 271736 (bytes into file) - Flags: 0x0 - Size of this header: 64 (bytes) - Size of program headers: 56 (bytes) - Number of program headers: 4 - Size of section headers: 64 (bytes) - Number of section headers: 15 - Section header string table index: 14 -``` - -更多关于 ELF 格式的细节请参考 [ELF - OSDev Wiki](https://wiki.osdev.org/ELF)。 - -### Linker Script - -在构建的最后一步,即链接产生 `build/kernel.img` 时,ChCore 构建系统中指定了使用从 `kernel/arch/aarch64/boot/linker.tpl.ld` 模板产生的 linker script 来精细控制 ELF 加载后程序各分段在内存中布局。 - -具体地,将 `${init_objects}`(即 `kernel/arch/aarch64/boot/raspi3` 中的代码编成的目标文件)放在了 ELF 内存的 `TEXT_OFFSET`(即 `0x80000`)位置,`.text`(代码段)、 `.data`(数据段)、`.rodata`(只读数据段)和 `.bss`(BSS 段)依次紧随其后。 - -这里对这些分段所存放的内容做一些解释: - -- `init`:内核启动阶段代码和数据,因为此时还没有开启 MMU,内核运行在低地址,所以需要特殊处理 -- `.text`:内核代码,由一条条的机器指令组成 -- `.data`:已初始化的全局变量和静态变量 -- `.rodata`:只读数据,包括字符串字面量等 -- `.bss`:未初始化的全局变量和静态变量,由于没有初始值,因此在 ELF 中不需要真的为该分段分配空间,而是只需要记录目标内存地址和大小,在加载时需要初始化为 0 - -除了指定各分段的顺序和对齐,linker script 中还指定了它们运行时“认为自己所在的内存地址”和加载时“实际存放在的内存地址”。例如前面已经说到 `init` 段被放在了 `TEXT_OFFSET` 即 `0x80000` 处,由于启动时内核运行在低地址,此时它“认为自己所在的地址”也应该是 `0x80000`,而后面的 `.text` 等段则被放在紧接着 `init` 段之后,但它们在运行时“认为自己在” `KERNEL_VADDR + init_end` 也就是高地址。 - -更多关于 linker script 的细节请参考 [Linker Scripts](https://sourceware.org/binutils/docs/ld/Scripts.html)。 diff --git a/Lab1/filelist.mk b/Lab1/filelist.mk new file mode 100644 index 00000000..8939a8aa --- /dev/null +++ b/Lab1/filelist.mk @@ -0,0 +1,5 @@ +BOOT := kernel/arch/aarch64/boot/raspi3 + +FILES := $(BOOT)/init/tools.S \ + $(BOOT)/peripherals/uart.c \ + $(BOOT)/init/mmu.c diff --git a/Lab1/scripts/grade/expects/lab1.exp b/Lab1/grade.exp similarity index 100% rename from Lab1/scripts/grade/expects/lab1.exp rename to Lab1/grade.exp diff --git a/Lab1/scripts/format/cmake_format_config.py b/Lab1/scripts/format/cmake_format_config.py deleted file mode 100644 index 297f7bb0..00000000 --- a/Lab1/scripts/format/cmake_format_config.py +++ /dev/null @@ -1,282 +0,0 @@ -# Copyright (c) 2023 Institute of Parallel And Distributed Systems (IPADS), Shanghai Jiao Tong University (SJTU) -# Licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. - -# ---------------------------------- -# Options affecting listfile parsing -# ---------------------------------- -with section("parse"): - - # Specify structure for custom cmake functions - additional_commands = { - 'chcore_add_subproject': { - 'flags': [ - 'BAR', - 'BAZ', - ], - 'kwargs': { - 'PREFIX': '*', - 'TMP_DIR': '*', - 'STAMP_DIR': '*', - 'LOG_DIR': '*', - 'DOWNLOAD_DIR': '*', - 'SOURCE_DIR': '*', - 'BINARY_DIR': '*', - 'INSTALL_DIR': '*', - 'BUILD_IN_SOURCE': '*', - 'BUILD_BYPRODUCTS': '*', - 'BUILD_ALWAYS': '*', - 'PATCH_COMMAND': '*', - 'CONFIGURE_COMMAND': '*', - 'BUILD_COMMAND': '*', - 'INSTALL_COMMAND': '*', - 'CMAKE_ARGS': '*', - 'CMAKE_CACHE_ARGS': '*', - 'DEPENDS': '*', - } - }, - 'chcore_enable_clang_tidy': { - 'kwargs': { - 'EXTRA_CHECKS': '*', - } - } - } - - # Override configurations per-command where available - override_spec = {} - - # Specify variable tags. - vartags = [] - - # Specify property tags. - proptags = [] - -# ----------------------------- -# Options affecting formatting. -# ----------------------------- -with section("format"): - - # Disable formatting entirely, making cmake-format a no-op - disable = False - - # How wide to allow formatted cmake files - line_width = 80 - - # How many spaces to tab for indent - tab_size = 4 - - # If true, lines are indented using tab characters (utf-8 0x09) instead of - # space characters (utf-8 0x20). In cases where the layout would - # require a fractional tab character, the behavior of the fractional - # indentation is governed by - use_tabchars = False - - # If is True, then the value of this variable indicates how - # fractional indentions are handled during whitespace replacement. If set to - # 'use-space', fractional indentation is left as spaces (utf-8 0x20). If set - # to `round-up` fractional indentation is replaced with a single tab character - # (utf-8 0x09) effectively shifting the column to the next tabstop - fractional_tab_policy = 'use-space' - - # If an argument group contains more than this many sub-groups (parg or kwarg - # groups) then force it to a vertical layout. - max_subgroups_hwrap = 2 - - # If a positional argument group contains more than this many arguments, then - # force it to a vertical layout. - max_pargs_hwrap = 6 - - # If a cmdline positional group consumes more than this many lines without - # nesting, then invalidate the layout (and nest) - max_rows_cmdline = 2 - - # If true, separate flow control names from their parentheses with a space - separate_ctrl_name_with_space = False - - # If true, separate function names from parentheses with a space - separate_fn_name_with_space = False - - # If a statement is wrapped to more than one line, than dangle the closing - # parenthesis on its own line. - dangle_parens = False - - # If the trailing parenthesis must be 'dangled' on its on line, then align it - # to this reference: `prefix`: the start of the statement, `prefix-indent`: - # the start of the statement, plus one indentation level, `child`: align to - # the column of the arguments - dangle_align = 'prefix' - - # If the statement spelling length (including space and parenthesis) is - # smaller than this amount, then force reject nested layouts. - min_prefix_chars = 4 - - # If the statement spelling length (including space and parenthesis) is larger - # than the tab width by more than this amount, then force reject un-nested - # layouts. - max_prefix_chars = 10 - - # If a candidate layout is wrapped horizontally but it exceeds this many - # lines, then reject the layout. - max_lines_hwrap = 2 - - # What style line endings to use in the output. - line_ending = 'unix' - - # Format command names consistently as 'lower' or 'upper' case - command_case = 'canonical' - - # Format keywords consistently as 'lower' or 'upper' case - keyword_case = 'unchanged' - - # A list of command names which should always be wrapped - always_wrap = [] - - # If true, the argument lists which are known to be sortable will be sorted - # lexicographicall - enable_sort = True - - # If true, the parsers may infer whether or not an argument list is sortable - # (without annotation). - autosort = False - - # By default, if cmake-format cannot successfully fit everything into the - # desired linewidth it will apply the last, most agressive attempt that it - # made. If this flag is True, however, cmake-format will print error, exit - # with non-zero status code, and write-out nothing - require_valid_layout = False - - # A dictionary mapping layout nodes to a list of wrap decisions. See the - # documentation for more information. - layout_passes = {} - -# ------------------------------------------------ -# Options affecting comment reflow and formatting. -# ------------------------------------------------ -with section("markup"): - - # What character to use for bulleted lists - bullet_char = '*' - - # What character to use as punctuation after numerals in an enumerated list - enum_char = '.' - - # If comment markup is enabled, don't reflow the first comment block in each - # listfile. Use this to preserve formatting of your copyright/license - # statements. - first_comment_is_literal = False - - # If comment markup is enabled, don't reflow any comment block which matches - # this (regex) pattern. Default is `None` (disabled). - literal_comment_pattern = None - - # Regular expression to match preformat fences in comments default= - # ``r'^\s*([`~]{3}[`~]*)(.*)$'`` - fence_pattern = '^\\s*([`~]{3}[`~]*)(.*)$' - - # Regular expression to match rulers in comments default= - # ``r'^\s*[^\w\s]{3}.*[^\w\s]{3}$'`` - ruler_pattern = '^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$' - - # If a comment line matches starts with this pattern then it is explicitly a - # trailing comment for the preceeding argument. Default is '#<' - explicit_trailing_pattern = '#<' - - # If a comment line starts with at least this many consecutive hash - # characters, then don't lstrip() them off. This allows for lazy hash rulers - # where the first hash char is not separated by space - hashruler_min_length = 10 - - # If true, then insert a space between the first hash char and remaining hash - # chars in a hash ruler, and normalize its length to fill the column - canonicalize_hashrulers = True - - # enable comment markup parsing and reflow - enable_markup = False - -# ---------------------------- -# Options affecting the linter -# ---------------------------- -with section("lint"): - - # a list of lint codes to disable - disabled_codes = [] - - # regular expression pattern describing valid function names - function_pattern = '[0-9a-z_]+' - - # regular expression pattern describing valid macro names - macro_pattern = '[0-9A-Z_]+' - - # regular expression pattern describing valid names for variables with global - # (cache) scope - global_var_pattern = '[A-Z][0-9A-Z_]+' - - # regular expression pattern describing valid names for variables with global - # scope (but internal semantic) - internal_var_pattern = '_[A-Z][0-9A-Z_]+' - - # regular expression pattern describing valid names for variables with local - # scope - local_var_pattern = '[a-z][a-z0-9_]+' - - # regular expression pattern describing valid names for privatedirectory - # variables - private_var_pattern = '_[0-9a-z_]+' - - # regular expression pattern describing valid names for public directory - # variables - public_var_pattern = '[A-Z][0-9A-Z_]+' - - # regular expression pattern describing valid names for function/macro - # arguments and loop variables. - argument_var_pattern = '[a-z][a-z0-9_]+' - - # regular expression pattern describing valid names for keywords used in - # functions or macros - keyword_pattern = '[A-Z][0-9A-Z_]+' - - # In the heuristic for C0201, how many conditionals to match within a loop in - # before considering the loop a parser. - max_conditionals_custom_parser = 2 - - # Require at least this many newlines between statements - min_statement_spacing = 1 - - # Require no more than this many newlines between statements - max_statement_spacing = 2 - max_returns = 6 - max_branches = 12 - max_arguments = 5 - max_localvars = 15 - max_statements = 50 - -# ------------------------------- -# Options affecting file encoding -# ------------------------------- -with section("encode"): - - # If true, emit the unicode byte-order mark (BOM) at the start of the file - emit_byteorder_mark = False - - # Specify the encoding of the input file. Defaults to utf-8 - input_encoding = 'utf-8' - - # Specify the encoding of the output file. Defaults to utf-8. Note that cmake - # only claims to support utf-8 so be careful when using anything else - output_encoding = 'utf-8' - -# ------------------------------------- -# Miscellaneous configurations options. -# ------------------------------------- -with section("misc"): - - # A dictionary containing any per-command configuration overrides. Currently - # only `command_case` is supported. - per_command = {} - diff --git a/Lab1/scripts/format/format.sh b/Lab1/scripts/format/format.sh deleted file mode 100755 index 7674004a..00000000 --- a/Lab1/scripts/format/format.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/bash -# Copyright (c) 2023 Institute of Parallel And Distributed Systems (IPADS), Shanghai Jiao Tong University (SJTU) -# Licensed under the Mulan PSL v2. -# You can use this software according to the terms and conditions of the Mulan PSL v2. -# You may obtain a copy of Mulan PSL v2 at: -# http://license.coscl.org.cn/MulanPSL2 -# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR -# PURPOSE. -# See the Mulan PSL v2 for more details. - -set -e - -script_dir=$(dirname "$0") - -RED='\033[0;31m' -BLUE='\033[0;34m' -GREEN='\033[0;32m' -ORANGE='\033[0;33m' -BOLD='\033[1m' -NONE='\033[0m' -C_SOURCES=".*\.(c|h)$" -CPP_SOURCES=".*\.(cpp|cc|hpp|cxx)$" - -str="" -cnt=0 - -main() { - for mod in "$@"; do - if [ $cnt -eq 1 ]; then - let cnt=$cnt+1 - str=""$mod; - elif [ $cnt -gt 1 ]; then - str=$str"\|"$mod; - fi - if [ $mod == "-exclude" ]; then - let cnt=$cnt+1 - fi - done - for mod in "$@"; do - if [ $mod == "-exclude" ]; then - break - fi - if [ $cnt -gt 0 ]; then - for file in $(find $mod -type f | grep -v $str); do - format $file - done - else - for file in $(find $mod -type f); do - format $file - done - fi - done - echo "============" - echo "Done" -} - -format() { - type=$(file $1) - if [[ $1 =~ $C_SOURCES || $1 =~ $CPP_SOURCES ]]; then - echo "Formatting C or C++ file \"$file\"..." - clang-format -i -style=file $1 - clang-format -i -style=file $1 # run clang-format twice in case of a bug - elif (echo $type | grep -q "Bourne-Again shell script"); then - echo "Formatting Bash script \"$file\"..." - shfmt -i 4 -w $1 - elif (echo $type | grep -q "Python script"); then - echo "Formatting Python script \"$file\"..." - black -q $1 - elif (echo $1 | grep -q "CMakeLists.txt") || (echo $1 | grep -q "*.cmake"); then - echo "Formatting CMake \"$file\"..." - cmake-format -c $script_dir/cmake_format_config.py -i $1 - fi -} - -print_usage() { - echo -e "\ -${BOLD}Usage:${NONE} ./scripts/format/format.sh [path1] [path2] ... -exclude [keyword1] [keyword2] ... - -${BOLD}Supported Languages:${NONE} - C/C++ - Bash - Python - CMake - -${BOLD}Examples:${NONE} - ./scripts/format/format.sh kernel/ipc - ./scripts/format/format.sh user/system-servers/fsm user/system-servers/procmgr user/init/main.c - ./scripts/format/format.sh kernel -exclude arch - ./scripts/format/format.sh kernel -exclude kernel/arch/aarch64 -" -} - -if [ $# -eq 0 ]; then - print_usage - exit -fi - -if [ -f /.dockerenv ]; then - # we are in docker container - main $@ -else - echo "Starting docker container to do formatting" - docker run -it --rm \ - -u $(id -u ${USER}):$(id -g ${USER}) \ - -v $(pwd):/chos -w /chos \ - ipads/chcore_formatter:v2.0 \ - ./scripts/format/format.sh $@ -fi diff --git a/Lab1/scripts/format/formatter/Dockerfile b/Lab1/scripts/format/formatter/Dockerfile deleted file mode 100644 index 5dc7edb3..00000000 --- a/Lab1/scripts/format/formatter/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -# Dockerfile for ipads/chcore_formatter. - -FROM ubuntu:20.04 - -ENV DEBIAN_FRONTEND=noninteractive -ENV TZ=Asia/Shanghai -RUN apt-get update - -# --- Formatters installed by APT --- - -# Install clang-format to format C/C++ code -RUN apt-get install -y clang-format=1:10.0-* && \ - clang-format --version - -# --- Formatters installed by CURL --- - -RUN apt-get install -y curl - -# Install shfmt to format shell scripts -ARG SHFMT_VERSION=3.4.1 -RUN curl -L -o /usr/local/bin/shfmt "https://github.com/mvdan/sh/releases/download/v${SHFMT_VERSION}/shfmt_v${SHFMT_VERSION}_linux_amd64" && \ - chmod +x /usr/local/bin/shfmt && \ - shfmt --version - -RUN apt-get remove -y --purge curl - -# --- Formatters installed by PIP --- - -RUN apt-get install -y python3 python3-pip - -# Install cmake-format to format CMake scripts -ARG CMAKE_FORMAT_VERSION=0.6.13 -RUN pip3 install "cmakelang==$CMAKE_FORMAT_VERSION" && \ - cmake-format --version - -# Install black to format Python -ARG BLACK_VERSION=21.10b0 -RUN pip3 install "black==$BLACK_VERSION" && \ - black --version - -# --- Clean up --- -RUN apt-get clean && rm -rf /var/lib/apt/lists/* diff --git a/Lab1/scripts/grade/lab1.sh b/Lab1/scripts/grade/lab1.sh deleted file mode 100755 index 95055705..00000000 --- a/Lab1/scripts/grade/lab1.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -make="${MAKE:-make}" - -$make distclean -$make defconfig -$make build - -RED='\033[0;31m' -BLUE='\033[0;34m' -GREEN='\033[0;32m' -ORANGE='\033[0;33m' -BOLD='\033[1m' -NONE='\033[0m' - -if ! type expect >/dev/null 2>&1; then - echo -e "${BOLD} Please install expect before grading. (e.g., sudo apt-get install expect) ${NONE}" - exit 1 -fi - -grade_dir=$(dirname $0) - -echo -e "${BOLD}===============${NONE}" -echo -e "${BLUE}Grading lab 1...(may take 10 seconds)${NONE}" - -$grade_dir/expects/lab1.exp -score=$? - -echo -e "${BOLD}===============${NONE}" -echo -e "${GREEN}Score: $score/100${NONE}" diff --git a/Lab6/Makefile b/Lab6/Makefile index 2961cd43..89206b89 100644 --- a/Lab6/Makefile +++ b/Lab6/Makefile @@ -1,4 +1,4 @@ -LAB := 5 +LAB := 6 V := @ PROJECT_DIR := . diff --git a/Pages/Appendix.md b/Pages/Appendix.md new file mode 100644 index 00000000..83a54825 --- /dev/null +++ b/Pages/Appendix.md @@ -0,0 +1 @@ +# 附录 diff --git a/Pages/Appendix/elf.md b/Pages/Appendix/elf.md new file mode 100644 index 00000000..81a8900e --- /dev/null +++ b/Pages/Appendix/elf.md @@ -0,0 +1,32 @@ +# ELF 文件格式 + +在Lab1中ChCore 的构建系统将会构建出 `build/kernel.img` 文件,该文件是一个 ELF 格式的“可执行目标文件”,和我们平常在 Linux 系统中见到的可执行文件如出一辙。ELF 可执行文件以 ELF 头部(ELF header)开始,后跟几个程序段(program segment),每个程序段都是一个连续的二进制块,其中又包含不同的分段(section),加载器(loader)将它们加载到指定地址的内存中并赋予指定的可读(R)、可写(W)、可执行(E)权限,并从入口地址(entry point)开始执行。 + +可以通过 `aarch64-linux-gnu-readelf` 命令查看 `build/kernel.img` 文件的 ELF 元信息(比如通过 `-h` 参数查看 ELF 头部、`-l` 参数查看程序头部、`-S` 参数查看分段头部等): + +```sh +$ aarch64-linux-gnu-readelf -h build/kernel.img +ELF Header: + Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 + Class: ELF64 + Data: 2's complement, little endian + Version: 1 (current) + OS/ABI: UNIX - System V + ABI Version: 0 + Type: EXEC (Executable file) + Machine: AArch64 + Version: 0x1 + Entry point address: 0x80000 + Start of program headers: 64 (bytes into file) + Start of section headers: 271736 (bytes into file) + Flags: 0x0 + Size of this header: 64 (bytes) + Size of program headers: 56 (bytes) + Number of program headers: 4 + Size of section headers: 64 (bytes) + Number of section headers: 15 + Section header string table index: 14 +``` + +更多关于 ELF 格式的细节请参考 [ELF - OSDev Wiki](https://wiki.osdev.org/ELF)。 + diff --git a/Pages/Appendix/linker.md b/Pages/Appendix/linker.md new file mode 100644 index 00000000..4913055a --- /dev/null +++ b/Pages/Appendix/linker.md @@ -0,0 +1,17 @@ +# Linker Script + +在Chcore 构建的最后一步,即链接产生 `build/kernel.img` 时,ChCore 构建系统中指定了使用从 `kernel/arch/aarch64/boot/linker.tpl.ld` 模板产生的 linker script 来精细控制 ELF 加载后程序各分段在内存中布局。 + +具体地,将 `${init_objects}`(即 `kernel/arch/aarch64/boot/raspi3` 中的代码编成的目标文件)放在了 ELF 内存的 `TEXT_OFFSET`(即 `0x80000`)位置,`.text`(代码段)、 `.data`(数据段)、`.rodata`(只读数据段)和 `.bss`(BSS 段)依次紧随其后。 + +这里对这些分段所存放的内容做一些解释: + +- `init`:内核启动阶段代码和数据,因为此时还没有开启 MMU,内核运行在低地址,所以需要特殊处理 +- `.text`:内核代码,由一条条的机器指令组成 +- `.data`:已初始化的全局变量和静态变量 +- `.rodata`:只读数据,包括字符串字面量等 +- `.bss`:未初始化的全局变量和静态变量,由于没有初始值,因此在 ELF 中不需要真的为该分段分配空间,而是只需要记录目标内存地址和大小,在加载时需要初始化为 0 + +除了指定各分段的顺序和对齐,linker script 中还指定了它们运行时“认为自己所在的内存地址”和加载时“实际存放在的内存地址”。例如前面已经说到 `init` 段被放在了 `TEXT_OFFSET` 即 `0x80000` 处,由于启动时内核运行在低地址,此时它“认为自己所在的地址”也应该是 `0x80000`,而后面的 `.text` 等段则被放在紧接着 `init` 段之后,但它们在运行时“认为自己在” `KERNEL_VADDR + init_end` 也就是高地址。 + +更多关于 linker script 的细节请参考 [Linker Scripts](https://sourceware.org/binutils/docs/ld/Scripts.html)。 diff --git a/Pages/Appendix/toolchains.md b/Pages/Appendix/toolchains.md new file mode 100644 index 00000000..9c6188e8 --- /dev/null +++ b/Pages/Appendix/toolchains.md @@ -0,0 +1,8 @@ +# 工具链教程 + +本教程会对实验中所需使用的一些命令行工具进行介绍,包括为什么要使用这些工具、宏观功能介 +绍以及部分工具的详细介绍,对于感兴趣的同学,我们也列出了一些更详细的资料供进一步学习。本教 +程中介绍的工具仅代表我们推荐的,在拆弹和后续ChCore实验中的常用命令行工具,但我们不限定同学 +们在完成实验时所使用的具体工具或方式,同学们可以使用任何其他自己熟悉的工具来完成实验。 +本教程中所介绍的工具和命令,均假设在Linux shell环境(如bash等)下执行。如果同学们希望使用其 +他操作系统或平台,请自行查阅相关工具的安装方法以及可能的命令语法不同之处。 diff --git a/Pages/Appendix/toolchains/assets/tmux.png b/Pages/Appendix/toolchains/assets/tmux.png new file mode 100644 index 00000000..608009a2 Binary files /dev/null and b/Pages/Appendix/toolchains/assets/tmux.png differ diff --git a/Pages/Appendix/toolchains/gdb.md b/Pages/Appendix/toolchains/gdb.md new file mode 100644 index 00000000..a0f9678a --- /dev/null +++ b/Pages/Appendix/toolchains/gdb.md @@ -0,0 +1,66 @@ +# GDB + + + +gdb是目前最常用的动态调试工具之一。所谓动态调试,指的是在程序运行的过程中对程序进行观测或 +施加干预的过程,一种常见的动态调试方法是断点,通过插入断点使得程序在特定点位暂停运行,让动 +态调试器可以对程序的状态进行进一步的观测。与动态调试相对的是静态分析。静态分析不需要实际运 +行程序,甚至不需要编译程序,而是在程序源代码或指令的层级进行一系列分析甚至“枚举”,预测程序 +的可能执行情况,寻找潜在的问题。某种程度上来说,对于存在问题的程序,程序员直接阅读代码或汇 +编指令并分析问题,也可以被视为一种静态分析。 + +相较于静态分析,动态调试可以真实地反映程序运行的实际情况,包括各种数据的实际值、程序的实际 +执行路径等。在某些场景下,使用动态调试寻找程序的问题或理解程序的行为,比直接阅读程序源码要 +简单许多,例如使用动态调试器单步运行程序,在每一步运行的前后观察程序的相关状态,可以非常直 +观地找到导致问题的程序指令或代码。当然,静态分析也有着广泛的应用,许多静态分析工具在无需编 +译或运行程序的情况下便可分析程序的潜在问题,这可以有效节约程序运行的时间,同时静态分析可以 +尽可能地枚举程序的可能执行路径,有助于发现实际运行程序时不会出现或非常罕见的问题。 + +## 源码级调试 vs 汇编级调试 + +动态调试是在程序的运行过程中施加干预和观测状态,问题在于,如何干预程序的运行?又该观测程序 +的什么状态?以打一个断点为例,应该在什么地方打断点,程序触发断点后,又该检查程序的哪些状 +态?动态调试器本身只是为程序员提供了完成上述工作的能力,但如何运用这些能力,仍然需要程序员 +本身对于程序的了解。站在程序员的角度,无疑希望能直接在程序执行到某一行源代码时触发断点,触 +发断点后,又可以直接检查程序中某个变量的值甚至复杂对象的内容。许多同学此前可能已经接触过各 +种IDE自带的调试功能,它们大多都允许程序员在源代码中设置断点,并且可以在触发断点时直接看到各 +个变量和对象的内容。这种程序员直接站在源代码的层级,使用源代码级的概念(代码行、变量、对象 +等)进行调试的过程称为源码级调试。 + +然而,从动态调试器的功能而言,要支持源码级调试并非仅有动态调试器即可做到。这是因为,CPU本 +身只能执行二进制形式的机器指令,不论是编译执行或是解释执行的高级编程语言,最终在程序运行 +时,动态调试器能观察和控制的只是最终的机器指令、寄存器和内存地址。例如,仅使用动态调试器本 +身,我们只能指定在某一条指令暂停执行,也不能直接检查变量或者对象的内容,因为在机器指令的层 +面并没有变量和对象的概念,只有寄存器和内存。这种只使用机器执行过程中直接可见的概念(指令、 +寄存器、内存)进行调试的过程称为汇编级调试/机器级调试。造成上述问题的原因是,在从源代码到可 +执行程序的编译或解释过程中,许多信息都丢失了,因为这些信息对于程序的最终执行并无任何帮助: +从程序执行的角度来说,CPU不需要理解某条指令对应源代码中的哪个文件的哪一行,也不需要理解某 +个寄存器在某一时刻存储的是哪个变量的值。但是,这些信息的丢失,就给调试带来了较大的困难,因 +为高级语言翻译成汇编指令的方式非常多样,并且存在各种复杂的细节,从而使得汇编级调试并不直 +观,往往需要远多于源码级调试的时间精力才有可能定位和理解程序存在的问题。 + +如果想要进行源码级调试,就需要在程序可执行文件中加入一系列的额外信息,来弥补编译/解释过程中 +损失的信息,让动态调试器可以把指令地址、寄存器、内存地址“还原”为源代码级的概念如源代码行、 +变量、对象等等。但是,嵌入这些信息会使得程序可执行文件的体积增大,所以如果不是在编译时使用 +特定的选项,程序往往是没有这些额外信息的。以Linux下最常见的可执行文件格式ELF为例,若要支持 +源码级调试,需要ELF文件存在符号表和专门的调试信息。其中符号表可以用于将一些内存地址还原回函 +数或全局变量等,除了调试之外,还有许多其他用途,而调试信息则是专门为了将机器级概念还原到源 +代码级存在的。如果一个ELF文件只有符号表,没有调试信息,那么绝大多数源码级调试功能也都是不可 +用的,但是也可以支持一定程度的源码级调试,例如在函数的入口打断点,检查全局变量的值等等。 +对于解释型语言,情况还要更复杂一些。这是因为,gdb等动态调试器,都是把程序视为一个“黑盒”,它 +们并不理解一个程序是在完成自身的工作,还是在作为解释器,为一种更高级的语言(如Python)提供 +支持。以Python为例,即使Python解释器程序本身有包含调试信息,从gdb的角度来看,也只能看到解 +释器本身的工作情况,例如它是如何解析Python字节码的,这个过程中它调用了自身的哪些函数,修改 +了自己内部的哪些变量等等。但是从Python程序员的角度来说,往往假设Python解释器本身是正确的, +问题在于自己编写的Python代码,比起理解解释器内部的执行情况,更关注的是Python语言层级的概 +念。但gdb等通用动态调试器是无法在Python语言层级进行调试的。对于使用解释执行的语言,需要解 +释器本身支持调试功能,往往还需要使用专门的调试器。 + +在本次实验中,我们提供的炸弹程序是使用非常短的C代码编译而成的,且没有使用过高的优化等级, +汇编指令与原始C代码是高度对应的;程序保留了符号表,但移除了调试信息。这是因为在操作系统中 +不可避免地存在无法使用高级语言,必须使用汇编语言编写的部分。因此我们希望通过一个复杂度有限 +的汇编程序,提高同学们对于汇编语言以及C语言编译到汇编语言过程的理解,同时增强同学们对gdb的 +熟悉程度和调试能力,为后续的实验打下基础。 + + +### 设置调试目标 diff --git a/Pages/Appendix/toolchains/gdb/comparison.md b/Pages/Appendix/toolchains/gdb/comparison.md new file mode 100644 index 00000000..b6cfa17f --- /dev/null +++ b/Pages/Appendix/toolchains/gdb/comparison.md @@ -0,0 +1,45 @@ +# 源码级调试 vs 汇编级调试 + +动态调试是在程序的运行过程中施加干预和观测状态,问题在于,如何干预程序的运行?又该观测程序 +的什么状态?以打一个断点为例,应该在什么地方打断点,程序触发断点后,又该检查程序的哪些状 +态?动态调试器本身只是为程序员提供了完成上述工作的能力,但如何运用这些能力,仍然需要程序员 +本身对于程序的了解。站在程序员的角度,无疑希望能直接在程序执行到某一行源代码时触发断点,触 +发断点后,又可以直接检查程序中某个变量的值甚至复杂对象的内容。许多同学此前可能已经接触过各 +种IDE自带的调试功能,它们大多都允许程序员在源代码中设置断点,并且可以在触发断点时直接看到各 +个变量和对象的内容。这种程序员直接站在源代码的层级,使用源代码级的概念(代码行、变量、对象 +等)进行调试的过程称为源码级调试。 + +然而,从动态调试器的功能而言,要支持源码级调试并非仅有动态调试器即可做到。这是因为,CPU本 +身只能执行二进制形式的机器指令,不论是编译执行或是解释执行的高级编程语言,最终在程序运行 +时,动态调试器能观察和控制的只是最终的机器指令、寄存器和内存地址。例如,仅使用动态调试器本 +身,我们只能指定在某一条指令暂停执行,也不能直接检查变量或者对象的内容,因为在机器指令的层 +面并没有变量和对象的概念,只有寄存器和内存。这种只使用机器执行过程中直接可见的概念(指令、 +寄存器、内存)进行调试的过程称为汇编级调试/机器级调试。造成上述问题的原因是,在从源代码到可 +执行程序的编译或解释过程中,许多信息都丢失了,因为这些信息对于程序的最终执行并无任何帮助: +从程序执行的角度来说,CPU不需要理解某条指令对应源代码中的哪个文件的哪一行,也不需要理解某 +个寄存器在某一时刻存储的是哪个变量的值。但是,这些信息的丢失,就给调试带来了较大的困难,因 +为高级语言翻译成汇编指令的方式非常多样,并且存在各种复杂的细节,从而使得汇编级调试并不直 +观,往往需要远多于源码级调试的时间精力才有可能定位和理解程序存在的问题。 + +如果想要进行源码级调试,就需要在程序可执行文件中加入一系列的额外信息,来弥补编译/解释过程中 +损失的信息,让动态调试器可以把指令地址、寄存器、内存地址“还原”为源代码级的概念如源代码行、 +变量、对象等等。但是,嵌入这些信息会使得程序可执行文件的体积增大,所以如果不是在编译时使用 +特定的选项,程序往往是没有这些额外信息的。以Linux下最常见的可执行文件格式ELF为例,若要支持 +源码级调试,需要ELF文件存在符号表和专门的调试信息。其中符号表可以用于将一些内存地址还原回函 +数或全局变量等,除了调试之外,还有许多其他用途,而调试信息则是专门为了将机器级概念还原到源 +代码级存在的。如果一个ELF文件只有符号表,没有调试信息,那么绝大多数源码级调试功能也都是不可 +用的,但是也可以支持一定程度的源码级调试,例如在函数的入口打断点,检查全局变量的值等等。 +对于解释型语言,情况还要更复杂一些。这是因为,gdb等动态调试器,都是把程序视为一个“黑盒”,它 +们并不理解一个程序是在完成自身的工作,还是在作为解释器,为一种更高级的语言(如Python)提供 +支持。以Python为例,即使Python解释器程序本身有包含调试信息,从gdb的角度来看,也只能看到解 +释器本身的工作情况,例如它是如何解析Python字节码的,这个过程中它调用了自身的哪些函数,修改 +了自己内部的哪些变量等等。但是从Python程序员的角度来说,往往假设Python解释器本身是正确的, +问题在于自己编写的Python代码,比起理解解释器内部的执行情况,更关注的是Python语言层级的概 +念。但gdb等通用动态调试器是无法在Python语言层级进行调试的。对于使用解释执行的语言,需要解 +释器本身支持调试功能,往往还需要使用专门的调试器。 + +在本次实验中,我们提供的炸弹程序是使用非常短的C代码编译而成的,且没有使用过高的优化等级, +汇编指令与原始C代码是高度对应的;程序保留了符号表,但移除了调试信息。这是因为在操作系统中 +不可避免地存在无法使用高级语言,必须使用汇编语言编写的部分。因此我们希望通过一个复杂度有限 +的汇编程序,提高同学们对于汇编语言以及C语言编译到汇编语言过程的理解,同时增强同学们对gdb的 +熟悉程度和调试能力,为后续的实验打下基础。 diff --git a/Pages/Appendix/toolchains/gdb/usage.md b/Pages/Appendix/toolchains/gdb/usage.md new file mode 100644 index 00000000..9e342d84 --- /dev/null +++ b/Pages/Appendix/toolchains/gdb/usage.md @@ -0,0 +1,139 @@ +# 使用简介 + +## 设置目标 + +gdb支持调试本地运行的进程,也支持通过网络等方式远程调试其他机器上运行的进程。在 +Linux机器上调试本地进程时,gdb依赖ptrace这个syscall,它从操作系统层面为gdb控制其 +他进程的运行提供了基础支持。一般而言,出于安全性考虑,各个Linux发行版都对ptrace +syscall的调用进行了程度不等的权限控制。例如只允许通过ptrace调试子进程等。 + +### 启动程序为子进程 + +> [!TIP] +> gdb `` + +其中, `` 为需要被调试的程序名,可以是一个完整的可执行文件路径,也可以 +只提供程序自身的可执行文件的名称。对于后一种情况,gdb会搜索$PATH环境变量来 +找到可执行文件的实际路径 + +执行该命令后,会进入gdb命令行。此时,gdb并不会立即开始执行被调试的程序。但此 +时gdb已经载入了可执行文件中的符号表和调试信息(如有),可以在实际执行程序前 + +就设置一些断点等,以便调试程序运行早期的代码或不接收输入的程序等等。 +确认完成准备工作后,在gdb命令行中执行 run 命令,作为gdb的子进程运行被调试程 +序。 + +### Attach 进入子进程 + +> [!TIP] +> sudo gdb -p `` + +> [!IMPORTANT] +> 一般而言,在常见Linux发行版上,直接attach到运行中进程可能需要root权限。 + +### 远程调试 + +gdb支持通过网络、串口等调试其他机器上运行的程序。这里我们以网络远程调试为例 +说明远程调试的基本原理。如果想使用网络远程调试,需要在实际运行被调试程序的机 +器上启动一个gdbserver,或任何实现了gdbserver协议的代理程序(以下统称为 +gdbserver)。gdb将会连接到这个gdbserver,并通过网络向gdbserver发送命令,以 +及通过gdbserver,读取运行中程序的信息等。在这个架构下,实际控制程序运行的是 +gdbserver,而不是gdb,但gdbserver又受到gdb的控制。远程调试的主要优点是提供 +了一种通用的将调试和程序的运行解耦的方法,而不是必须将被调试程序作为gdb的子 +进程运行。例如,远程调试可以允许程序在其他机器上运行,这对于一些必须在内网中 +运行或是依赖特殊硬件等的程序很有用;此外,远程调试基于gdbserver协议,任何程序 +只要实现了该协议,都可以“表现为”一个gdbserver,如此,程序可以主动将自己的一些 +内部信息暴露给gdb。理论上而言,这可以用于实现Python、Go等由虚拟机运行的程序 +的调试(虽然实际上这些语言的调试不是这么实现的),也可以用于调试由qemu运行 +的程序(详见下文)。 +使用 target remote :`` ,即可将gdb的调试目标设为远程的 +gdbserver。 + +如果通过gdb进行远程调试,在运行gdb时是否还需要指定 `` ?事实上,答案 +并非是完全不需要。如果只想进行纯粹的汇编级调试,的确可以不需要指定 +`` ,但如果想要进行源码级调试,则仍需要指定 `` 。在这种情况 +下, `` 的主要作用是让gdb读取其中的符号表和调试信息。gdb不会直接从 +gdbserver中读取这些信息,因为gdbserver不一定能提供这些信息,gdb只会和 +gdbserver交换汇编级的信息,它依赖本地 `` 文件中的符号表和调试信息将源 +码级信息翻译回汇编级信息。此外,还需要注意,在触发一个断点后,如果希望看到当 +前所执行到的程序源代码,则还需要本地保存了程序的源代码。这是因为,调试信息 +(以ELF所使用的DWARF格式为例)只保存了某条指令所对应的源代码的文件路径与行 +数,并没有直接保存源代码,gdb所做的工作是根据调试信息中的路径和行数,读取本 +地的代码文件并显示相应源代码。如果本地相应路径没有源代码文件或内容有误,那么 +gdb将无法正确显示源代码。 + +如果运行gdb时没有指定 `` ,还可以通过 add-symbol-file 命令指定 +`` 。如果程序不是在本地编译的,那么源代码的绝对路径可能与本地保存源代 +码的路径不同,可以用 set-substitute-path 命令进行替换。 + +## 断点控制 + +- break `` : 在 `` 处创建一个断点。 `` 是一个最终可以求值为某条指令的地址的表达式。例如,如果想直接在某个地址处设置断点,可以写成 *`
` ,如果被调试程序有符号表,可以直接使用函数名。如果被调试程序有调试信息,可以指定在某个源文件的某一行设置断点,甚至可以使用更复杂的C表达式,更详细的信息可以参考手册。但需要注意,不论使用何种表达式,最终其实都是求值到一条指令的地址,gdb只是利用调试信息并帮助程 +序员简化了这一步骤。 +- info breakpoints : 列出当前的所有断点 +- disable/enable `` : 禁用/启用编号为 `` 的断点,断点编号可以通过前述 info 命令查看。 +- delete `` : 删除编号为 `` 的断点。 + +## 执行控制 + +- 不输入任何命令,直接按下回车键:重复上一条命令 +- 在触发断点后,可以通过下列命令控制如何恢复程序的执行 +- continue : 恢复程序执行,直到触发下一个断点 +- continue `` : 恢复程序执行,且忽略此断点 `` 次 +- kill : 终止程序执行 +- quit : 退出gdb +- stepi : 执行下一条机器指令,随后继续暂停执行(单步调试) +- stepi `` : 执行接下来 `` 条指令 +- step : 执行下一条语句,这属于源代码级调试,需要调试信息 +- nexti : 执行下一条指令,且不跟踪(step through)函数调用。与 stepi 不同,如果当前指 +- 令是一条 bl 等指令,那么 stepi 会在被调用函数的第一条指令暂停(step in),而 nexti +- 会在 bl 指令之后的那条指令,即被调用函数返回后的那条指令上暂停。 +- nexti `` , next : 与 stepi , step 类似,只是不跟踪函数调用。 +- finish : 恢复执行直到当前被调用的函数返回 + +## 显示信息 + +- backtrace : 显示栈跟踪信息,可以看到当前函数是如何一步步被调用到的。但需要注意,由 +于编译器的优化等因素,如果没有调试信息,栈跟踪信息可能是不准确的,甚至无法提供栈跟 +踪信息。 + +- print`` `` : 打印表达式 `` 执行的值。其中 `` 可以是寄存器,如 $pc , +$sp , $x0 ,也可以是C表达式等。如果没有调试信息,则所使用的C表达式不能涉及到程序中 +变量的值,但仍可以使用常量或寄存器中的值,例如,可以直接将某个内存地址作为数字打 +印: print *(int *)0x1234 ,或使用寄存器中的值参与运算等: print $x0+$x1 。 +`` 是可选的修饰符,可以用来控制打印的格式,详细信息可以参考手册。表达式求值 +时,寄存器、变量、内存中的值都基于触发断点时程序的状态。 + +- x/NFU `
` : 打印内存中指定长度的内容。与 print 命令不同,该命令只能打印内存 +中的内容,且不需要如print一样将内存地址转换为指针并求值。这是因为此命令只是将内存 +视为字节数组,打印出其中给定数目的字节的内容,而不对字节内容做任何解释。相反, +print 在打印指针求值表达式时,会根据指针的类型信息和 `` 来计算需要打印多少字 +节,且会对多字节的内容进行解释,例如将它们显示为一个完整的整数,而非整数在内存中的 +各个字节。此命令的修饰符中, N 表示需要打印的单元数目, U 的取值可以为 b , h , w , g ,分 +别表示一个单元大小为1,2,4,8字节。 F 表示每个单元的打印格式,如 i 表示作为指令反 +汇编, x 表示十六进制, d 表示十进制等,详细信息可以查看手册。 + +- display`` `` : 在每次程序暂停执行时,自动根据 `` 打印 `` 的值,适 +用于一些频繁更改的表达式。利用这条命令可以在单步调试时自动查看所需关注的值,而无需 +反复输入print命令。 + +- info display : 列出所有的自动打印。 + +- delete display `` : 删除编号为 `` 的自动打印 + +## TUI + +> [!NOTE] +> TUI(text UI)是gdb内置的高级命令行界面功能。TUI在gdb内部引入了类似窗口的概念,允许用户在使用gdb命令的同时查看被调试程序的汇编指令、源代码等,也允许用户设置自定义的UI布局,提高工作效率。 + +- lay asm : 进入TUI默认汇编布局,此布局下会同时开启一个gdb命令窗口以及一个汇编指令 +窗口,用户可以直观看到当前正在执行的汇编指令。 +- lay src : 进入TUI默认源代码布局,此布局下会同时开启gdb命令窗口和源代码窗口。需要 +具备调试信息和源代码文件。 +- tui disable : 退出TUI。 + +## 扩展阅读 + +- [ptrace syscall](https://man7.org/linux/man-pages/man2/ptrace.2.html) +- [gdb手册](https://sourceware.org/gdb/onlinedocs/gdb/index.html#SEC_Contents)(如想深入了解gdb或参考命令的详细用法,非常建议阅读,此文档只是给出了一个非常简要的介绍):TUI: +- [TUI](https://sourceware.org/gdb/onlinedocs/gdb/TUI.html) diff --git a/Pages/Appendix/toolchains/make.md b/Pages/Appendix/toolchains/make.md new file mode 100644 index 00000000..27d4e612 --- /dev/null +++ b/Pages/Appendix/toolchains/make.md @@ -0,0 +1,58 @@ +# Make + + + +## 简介 + +make是Linux操作系统上常见的较为基础的构建系统(build system)。构建系统的主要工作,是管理 +一个较大规模或较为复杂的项目的各个“部件”,以及如何将这些“部件”“组装”成为最终的项目产物。以C +语言项目的编译过程为例。一个最简单的C语言项目,只需要有一个源文件即可,这种简单的项目,确 +实对构建系统没有很强的需求,只需要一条很简短的gcc命令即可完成编译。但是,随着项目规模的增 +加,项目的源文件也会越来越多,诚然,我们可以把所有源文件的文件名都提交给gcc命令进行编译,但 +是这样会导致每次都要输入一个很长的命令。或许有同学会说,命令历史记录或者写一个简单的脚本都 +可以简化这个操作。但是构建系统能做的不仅如此。如果每次都把所有源文件提交给gcc让它进行编译, +那么gcc每次都会执行一次完整编译 ,即重新将每一个源文件编译成对象文件,再将他们进行链接。然 +而,事实上,在一个规模较大的项目中,我们在更新这个项目时,通常不会修改项目的所有源文件,常 +常只有一小部分或者一部分源文件被修改了,那么那些没有被修改的源文件就完全没有必要再重新编 +译,只需要将已有的中间产物 (对象文件)与修改过的文件重新编译后的对象文件进行链接即可,也即 +所谓的部分编译 。换句话说,如果每次都使用前述的一条完整gcc命令,那么相当一部分编译时间就被 +浪费了,因为gcc自身并不理解哪些文件需要重新编译,哪些文件不需要重新编译,而且除非添加特定参 +数,gcc也不会保留中间产物或利用现有的中间产物。相比于在一条命令中把所有源文件都提交给gcc, +更好的做法是把整个构建过程拆散,先逐个将源文件编译为对象文件,并且保存这些对象文件作为中间 +产物,随后再将这些中间产物链接到一起,产生完整的程序。当然,这么做相较于一条gcc命令会复杂很 +多,但对于大型项目可以显著减少编译时间。由于整个流程从一条命令变成了需要以特定顺序执行的许 +多条命令,因此出于可维护性的考虑,应当把整个流程以文件形式保存下来,这就是构建系统中常见的 +规则文件 。 + +如前所述,一个比较复杂的项目,其编译流程涉及多条(甚至是大量)需要按照特定顺序执行的命令。 +如何确定这些命令的顺序?如何在项目的结构发生变更后重新确定这些命令执行的顺序?这些问题,是 +单纯的shell脚本所不能解决的,因此构建系统的规则文件通常并不是简单的shell脚本。一般而言,构建 +系统的规则文件都包含几个基本元素(不同系统中的术语可能不同),以make为例:目标(target)、 +配方(recipe)、前置条件(prerequisites)。目标指的是构建系统需要负责生成的一个文件或完成的 +一项任务,这个文件可以是某个中间产物文件,也可以是最终的产物或任何其他文件;此外,它也可以 +是一项抽象的任务,例如本实验中用到的使用qemu运行炸弹程序,也被作为make的一个目标。前置条 +件指的是在完成这个目标前需要完成的其他目标,一个目标只有在前置目标都已完成时才能被执行。最 +简单的前置目标是构建这个目标所需的源文件,例如用于生成对象文件app.o的目标,其前置目标可以 +是其源文件app.c。构建系统可以根据文件系统中app.o和app.c两个文件的修改时间,来判断是否需要 +执行这个目标,如果app.c在app.o产生之后又被修改过,则认为app.o需要重新生成,否则就可以直接 +使用现有的app.o。配方则是定义如何完成某一个特定的目标,通常包含一系列shell命令等。从上面的描述中不难看到,规则文件的作用实际上是定义了一个依赖图 ,图中每个目标是一个顶点,如果目标A依赖目标B,则目标B有一条边指向目标A。从原理上来说,构建系统会根据规则文件构建出依赖图,随 +后依照图的拓扑序执行每个顶点所定义的配方,最终就能生成完整的产物。还需要注意,构建系统是独 +立于编译器之外的,构建系统能做的,是给程序员提供自动生成和执行依赖图的能力,但依赖图的结构 +本身仍然是规则文件的编写者定义的,例如,项目的构建涉及到多少目标,每个目标的配方使用什么命 +令,每个目标的前置条件是什么,这些问题是不可能由构建系统自身来解决的,而是要由规则文件的编 +写者“告知”构建系统。 + +**总而言之,使用构建系统而不是直接调用编译器,对于复杂C/C++项目有下列好处:** + +- 增强构建流程可维护性 +- 简化执行部分任务(类似于脚本),例如清理构建中间产物等 +- 通过部分编译/增量编译,可以显著减少大规模项目的编译时间 +- 可以并发执行所有前置已被满足的目标(并发编译),有效利用多核CPU,进一步缩短编译时间 + +## 扩展阅读 + +> [!TIP] +> make是一个比较底层的构建系统,在使用上仍有诸多繁琐之处,于是又出现了一些用于替代make或在make更上层,更为简化的构建系统,前者的代表项目之一是ninja,后者的代表项目之一是CMake,感兴趣的同学可以进一步了解。 + +- +- diff --git a/Pages/Appendix/toolchains/objdump.md b/Pages/Appendix/toolchains/objdump.md new file mode 100644 index 00000000..00ae7478 --- /dev/null +++ b/Pages/Appendix/toolchains/objdump.md @@ -0,0 +1,59 @@ +# Objdump + + + +## 简介 + +objdump是GNU binutils中的标准工具之一。它的主要作用是显示二进制程序的相关信息,包括可执行 +文件、静态库、动态库等各种二进制文件。因此,它在逆向工程和操作系统开发等底层领域中非常常 +用,因为在这些领域中,我们所接触到的程序很可能不提供源代码,只有二进制可执行文件;抑或是所 +面临的问题在高级语言的抽象中是不存在的,只有深入到机器指令的层次才能定位和解决。在这些领域 +中,objdump最常见的应用是反汇编,即将可执行文件中的二进制指令,根据目标架构的指令编码规 +则,还原成文本形式的汇编指令,以供用户从汇编层面分析和理解程序。例如,在拆弹实验中,我们只 +提供了炸弹程序的极少一部分源代码,同学们需要通过objdump等工具进行逆向工程,从汇编层级理解 +炸弹程序的行为,进而完成实验。 + +## GNU实现 vs LLVM实现 + +objdump最初作为GNU binutils的一部分发布的。由于它直接处理二进制指令,因此它的功能与CPU架 +构等因素强相关。在常见Linux发行版中,通过包管理安装的objdump均为GNU binutils提供的实现, +且它一般被编译为用于处理当前CPU架构的二进制指令。例如,在x86_64 CPU上运行的Linux中安装 +objdump,它一般只能处理编译到x86_64架构的二进制文件。对于GNU binutils提供的实现,如果需要 +处理非当前CPU架构的二进制可执行文件,则需要额外安装为其他架构编译的objdump,例如,如果需 +要在x86_64 CPU上处理aarch64架构的二进制文件,一般需要使用 aarch64-linux-gnu-objdump 。 +LLVM是另一个被广泛应用的模块化编译器/工具链项目集合。LLVM项目不仅提供了另一个非常常见的 +C/C++编译器 clang ,也提供了一组自己的工具链实现,对应GNU binutils中的相关工具。例如,LLVM +也提供了自己的objdump实现,在许多Linux发行版上,你需要安装和使用 llvm-objdump 命令。LLVM +所提供的工具链实现与GNU binutils中的相应工具是基本兼容的,这意味着它们的命令行参数/语法等是 +相同的,但LLVM工具链也提供了其他扩展功能。从用户的角度来说,LLVM工具链与GNU binutils的一 +个主要区别是LLVM工具链不需要为每个能处理的架构编译一个单独的版本,以 llvm-objdump 为例,不 +论处理x86_64还是aarch64架构中的二进制文件,均可以使用 llvm-objdump 命令(需要在编译 llvm- +objdump 时开启相应的支持,从发行版包管理中安装的版本通常有包含)。 + +## 可执行文件格式 + +为了更好地加载和执行程序,操作系统在一定程度上需要了解程序的内部结构和一系列元数据。因此, +一个二进制可执行文件并不仅仅是将所有指令和数据按顺序写到文件中即可,通常它需要以一种特定的 +可执行文件格式存储,使用的格式是由它将要运行在的操作系统决定的。例如,对于Unix/Linux操作系 +统,ELF格式是目前最主流的可执行文件格式。而Windows下主要的可执行文件格式是PE/COFF。除了 +反汇编之外,objdump还可以显示与可执行文件格式相关的众多信息,但需要用户对可执行文件格式自 +身的理解。详细分析可执行文件格式超出了本教程的范围,感兴趣的同学建议查阅与ELF相关的资料。在 +后续ChCore实验中,也会涉及部分关于ELF的知识。 + +## objdump使用 + +本节只介绍拆弹实验中可能用到的基础用法。详细用法感兴趣的同学可以参考扩展阅读部分。 + +- `objdump -dS a.out a.S` : 反汇编`a.out`中的可执行section(不含数据 +sections),并保存到输出文件`a.S`中。在可能的情况下(如有调试信息和源文件),还会输出汇编指 +令所对应的源代码。 + +- `objdump -dsS a.out a.S` : 将`a.out` sections的内容全部导出,但 +仍然只反汇编可执行sections,且在可能情况下输出源代码。 + +## 扩展阅读 + +- [objdump手册](https://man7.org/linux/man-pages/man1/objdump.1.html) +- +- +- diff --git a/Pages/Appendix/toolchains/qemu.md b/Pages/Appendix/toolchains/qemu.md new file mode 100644 index 00000000..0e40eb75 --- /dev/null +++ b/Pages/Appendix/toolchains/qemu.md @@ -0,0 +1,9 @@ +# QEMU + +qemu是目前广泛应用的开源模拟器和虚拟机项目。它可以在一种架构的CPU(如x86)上,模拟其他多 +种架构的CPU(如aarch64等),这使得可以通过qemu在x86 CPU上运行为其他CPU架构编译的程序。 +例如,本实验的目的是让同学们熟悉aarch64汇编,因此我们提供的炸弹程序是为aarch64 CPU编译 +的,如果没有qemu,则程序不能在x86 CPU上执行。此外,qemu也可以模拟一个完整的机器,包括 +CPU、内存、磁盘以及多种其他外部硬件设备,此时它的功能基本等价于虚拟机,在这种场景下,它还 +可以与KVM技术配合,使用硬件虚拟化提高虚拟机的运行性能,但此时它就不能再运行为其他架构编译 +的操作系统了。 diff --git a/Pages/Appendix/toolchains/qemu/emulation.md b/Pages/Appendix/toolchains/qemu/emulation.md new file mode 100644 index 00000000..7d5e53b9 --- /dev/null +++ b/Pages/Appendix/toolchains/qemu/emulation.md @@ -0,0 +1,16 @@ +# 进程级模拟 vs 系统级模拟 + +qemu的模拟粒度可以分为进程级和系统级。在进程级模拟下,qemu只负责运行一个为其他架构编译的 +普通程序,这个程序与当前系统中运行的程序的唯一区别是它所使用的指令集不同,qemu会负责将它 +所使用的指令集翻译为当前机器可以执行的指令。除此之外,该程序相当于当前系统中的一个普通进 +程,它仍然通过当前系统的内核来访问操作系统提供的功能。默认情况下, qemu 命令执行进程级模 +拟。例如,我们提供的炸弹程序就只需要使用进程级模拟,它本质上就是一个普通的用户态程序,只是 +编译到了aarch64指令集而非x86指令集。除此之外,它的输入/输出等功能,仍然是调用当前操作系统 +提供的syscall。 + +qemu-system 命令可以用于系统级模拟。在系统级模拟下,QEMU会模拟一整套硬件,包括CPU、内 +存、磁盘以及多种可选硬件设备,此时QEMU的功能类似于虚拟机。在系统级模拟下,QEMU不能直接 +运行普通的用户态程序,而是需要运行完整的操作系统,由操作系统来管理QEMU模拟出的硬件资源。 +系统级模拟与是否使用KVM等硬件虚拟化进行加速是正交的。如果不使用KVM,QEMU仍然通过动态指 +令集翻译来运行被模拟的操作系统,此时它可以运行为其他架构编译的操作系统。否则,使用硬件虚拟 +化可以增强性能,但QEMU不再可以运行为其他架构编译的操作系统。 diff --git a/Pages/Appendix/toolchains/qemu/usage.md b/Pages/Appendix/toolchains/qemu/usage.md new file mode 100644 index 00000000..587d721c --- /dev/null +++ b/Pages/Appendix/toolchains/qemu/usage.md @@ -0,0 +1,34 @@ +# QEMU + GDB + +## GDBServer + +qemu实现了gdbserver协议,这使得gdb可以连接到qemu,并且给qemu发送指令,以及从qemu中读 +取信息。通过gdbserver,qemu可以把自己内部的信息暴露出来,或者说,可以让gdb理解它需要调试 +的目标是qemu中被模拟的程序(或操作系统),而不是qemu自身。例如,当gdb发送在地址 +0x400000 设置一个断点的指令时,qemu可以在被模拟程序执行到 0x400000 时暂停被模拟程序的执 +行,并告知gdb客户端,而不是在qemu自身执行到 0x400000 时暂停。如果gdb需要读取内存中的值, +qemu可以返回被调试程序的内存数据,而不是自身的内存数据。 +还需要强调,由于gdb和gdbserver之间交换的信息仅停留在汇编级,而且gdb完全使用本地的可执行程 +序文件中的符号表和调试信息执行源码级信息到汇编级信息之间的转换,因此在使用qemu系统级模拟 +时,我们可能会发现一些意料之外的行为。这是因为,在系统级模拟时,QEMU自身相当于站在CPU的 +抽象层面,而不是操作系统内核的抽象层面,这使得QEMU自身并不能理解它内部正在运行的操作系统 +的相关信息。例如,如果在qemu中运行一个Linux操作系统,其中的每一个进程的虚拟地址空间都有许 +多重叠,那么如果此时在 0x400000 打一个断点,qemu并不能识别出这个断点是针对哪个进程的,它所 +能做的只是在CPU“执行”到地址为 0x400000 的指令时暂停执行,不论当前执行的到底是哪个进程。又由 +于gdb客户端是完全根据用户所指定的可执行文件进行源码级到汇编级的翻译的,所以可能会出现这样 +一种情况: + +- gdb客户端从 appA 中读取符号表,其中,函数 funcA 的地址为 0x412345 ,它有2个参数 +- 设置断点: break funcA ,但实际上,gdb会根据符号表把 funcA 翻译回 0x412345 ,qemu的 +gdbserver只能接收到在 0x412345 打断点的指令。 +- 在qemu模拟的Linux操作系统中,进程 appB 恰好在执行,且它的函数 funcB (只有1个参数)的 +地址恰好也为 0x412345 ,qemu并不能理解这一点,它同样会暂停进程 appB 的执行。 +- 根据 appA 的调试信息,gdb客户端知道 funcA 有2个参数,所以它会要求gdbserver读取寄存器 +x0 和 x1 的值,用于显示 funcA 的参数。但由于此时暂停执行的实际上是只有1个参数的 funcB , +所以 x1 中保存的是一个随机值, x0 中保存的值很可能也不符合 funcA 的第一个参数的语义。于是 +我们会看到虽然触发了断点,但函数参数却像是乱码。 +- 同学们在后续chcore用户态相关的实验中,会对这个问题有更进一步的体会和理解。 + +## 扩展阅读 + +- diff --git a/Pages/Appendix/toolchains/tldr.md b/Pages/Appendix/toolchains/tldr.md new file mode 100644 index 00000000..94569275 --- /dev/null +++ b/Pages/Appendix/toolchains/tldr.md @@ -0,0 +1,41 @@ +# TL;DR Cheatsheet + +> [!INFO] +> <参数> 代表需要被替换的参数,请将其替换为你需要的实际值(包括左右两侧尖括号)。 + +--- + + + +## tmux + +- 创建新会话: tmux new -s <会话名> +- 进入会话: tmux attach -t <会话名> +- 临时退出会话(会话中程序保持在后台继续运行): Ctrl-b d +- 关闭会话及其中所有程序: tmux kill-session -t <会话名> +- 水平分屏: Ctrl-b " +- 垂直分屏: Ctrl-b % + +## gdb + +- 不输入任何命令,直接按下回车键:重复上一条命令 +- break *address : 在地址 address 处打断点 +- break <函数名> : 在函数入口处打断点 +- continue : 触发断点后恢复执行 +- info breakpoints : 列出所有断点 +- delete : 删除编号为 NUM 的断点 +- stepi : 触发断点后单步执行一条指令 +- print : 打印表达式 的求值结果,可以使用部分C语法,例如 `print*(int*) 0x1234` 可将地址 0x1234 开始存储的4个字节按32位有符号整数解释输出 +- print/x : 以16进制打印 的求值结果 +- lay asm : 使用TUI汇编视图 +- lay src : 使用TUI源代码视图(要求被调试可执行文件包含调试信息,且本地具有相应源代 +- 码文件) +- tui disable : 退出TUI + +## objdump操作 + +- objdump -dS <可执行文件> > <输出文件> : 反汇编可执行文件中的可执行section(不含数据 +- sections),并保存到输出文件中。在可能的情况下(如有调试信息和源文件),还会输出汇 +- 编指令所对应的源代码。 +- objdump -dsS <可执行文件> > <输出文件> : 将可执行文件中的所有sections的内容全部导 +- 出,但仍然只反汇编可执行sections,且在可能情况下输出源代码 diff --git a/Pages/Appendix/toolchains/tmux.md b/Pages/Appendix/toolchains/tmux.md new file mode 100644 index 00000000..fdc42ff2 --- /dev/null +++ b/Pages/Appendix/toolchains/tmux.md @@ -0,0 +1,113 @@ +# TMUX + + + +## 简介 + +如今大部分同学开始使用电脑接触的就是Windows与GUI(图形用户界面)。在GUI下,我们可以通过 +桌面和窗口的方式,管理显示器上的二维空间(桌面空间)。许多情况下,桌面空间对于单个应用程序 +来说,有些太大了,只使用一个应用程序不能有效地利用显示空间;又或者,我们想要同时处理多个任 +务,比如在写文档或者论文时,希望能非常方便地看到自己的参考资料……部分同学可能已经掌握如何使 +用分屏以及虚拟桌面来解决这些问题。的确,不论Windows10/11还是macOS抑或是Linux下的桌面环 +境,甚至众多基于Android的操作系统,分屏与虚拟桌面已经成为构建高效的GUI工作环境的基本功能。 +分屏解决的是如何有效利用桌面空间的问题。通过分屏功能的辅助,我们可以快速将多个应用程序的窗 +口紧密排列在桌面空间上,且可以更方便地调整这些窗口所占据的区域,避免自己手动排列带来的混乱 +和不便,让我们可以同时使用多个相关的应用程序,最大化利用桌面空间与提高效率。 + +虚拟桌面则解决的是如何有效隔离多种使用场景的问题,它是分屏功能的进一步衍生。我们经常需要同 +时处理几种不同性质的任务,例如,一边在编写文档或论文,一边还可能需要看QQ、微信和回复消息。 +如果只有一个桌面空间,要么不时切出和最小化QQ微信的窗口,要么把它们也使用分屏与文档窗口排列 +在一起,但这两种使用方式,或多或少都会影响需要专注的文档编写任务。此时,我们可以使用虚拟桌 +面。虚拟桌面是在物理桌面空间上“虚拟”出多个互不相干的桌面空间,每个桌面空间内都可以有自己的 +窗口布局。虽然同时只能使用一个虚拟桌面,但我们可以在多个虚拟桌面间快速切换。使用虚拟桌面 +后,我们可以将比较相关的一类程序的窗口放在同一个虚拟桌面中,其余不相干的程序则放在其他虚拟 +桌面中,如此,可以有效减少其他程序对于当前工作任务的干扰,同时又能在多种不同工作环境中快速 +切换。 + +分屏与虚拟桌面有效提高了GUI下的窗口管理效率。但是,窗口和桌面的概念,并非只能局限于GUI中。 +利用除了字母数字外的各种字符和颜色,我们同样可以在命令行用户界面(CLI)下“绘制”窗口,相较于 +通过命令行参数,窗口这种交互方式对用户更友好,更直观。同样地,在CLI下的窗口中,分屏和虚拟桌 +面需要解决的这些效率问题同样是存在的,也一样有着解决这些问题的需求。tmux(terminal +multiplexer)项目则是目前在CLI环境下这些问题的主要解决方案。顾名思义,它是一个“终端多路复用 +器”,如果说分屏和虚拟桌面是有效利用GUI中的桌面空间,tmux则主要是有效利用终端中的空间。这里 +的终端,可以是GUI下的终端模拟器,比如Windows Terminal,iTerm2等,也可以是运行在命令行模 +式下的Linux的显示器空间等等。 + +## tmux vs 多个终端模拟器窗口 + +- 同一窗口内部布局自由构建(部分终端模拟器也可实现) +- 统一管理多个窗口、便捷切换(开多个终端容易混淆,不便于随意切换) +- tmux可以在不使用时将进程保持在后台继续运行(detach,而终端模拟器一旦关闭就会杀死其中所有进程) +- tmux还是一个服务器,可以通过网络连接,如果有需要,可以允许其他人通过网络连接到你的 +- tmux界面中,实现网络协作(终端模拟器不支持) +- tmux支持高度自定义的配置,且有丰富的插件生态 + +## Session/Window/Pane + +> [!NOTE] +> 在理解了GUI下为什么需要有分屏和虚拟桌面后,类比GUI下的概念,可以很容易地理解tmux中的相关概念。 + +### Pane + +pane相当于GUI下的一个窗口。只不过相较于GUI下窗口可以自行自由移动,也可以使用分屏辅助 +排列,CLI下窗口还是基于字符的,所以tmux下的pane只能实现类似于GUI分屏的紧密排列,不能 +自由移动,也不能实现pane之间的重叠。 + +### Window + +window则相当于GUI下的虚拟桌面。一个window是一组pane的集合,不同的window拥有独立的pane以及pane的布局,且可以在多个window间通过快捷键快速切换。 + +### Session + +session是tmux特有的概念,它是一组window的集合,代表一个完整的工作环境。一般来说,不 +论我们通过显示器、Linux桌面环境下的终端模拟器、Windows或macOS上的终端模拟器+ssh…… +等方式访问Linux命令行,首先都是进入一个shell中,而并不能直接进入tmux。因此,在tmux +中,相较于“打开”和“关闭” tmux,我们更常说"attach","detach"到session。attach指的是从当前 +运行的shell进入一个tmux session的过程,而detach则是从tmux session离开,回到单个shell的 +过程。相较于“打开”和“关闭”,session+attach/detach有下列好处: + +- 在同一个终端中,也可以方便切换不同工作环境(这是最基础的功能) +- 避免同一个session中有过多不相干的window,降低切换效率 +- detach不是关闭session,detach后,session中运行的所有程序会在后台继续运行,且tmux会负责收集它们产生的输出,我们随时可以重新attach到某一个session,就可以看到其中程序最新的运行情况和历史输出,这对于某些需要长时间后台运行的任务是非常方便的。 +- 可以有多个终端同时attach到一个session,搭配tmux的服务器功能,可以实现向他人共享你的工作界面和环境,以及协作工作。 + +![tmux](assets/tmux.png) + +### 常用快捷键与子命令 + +tmux支持丰富的快捷键,同时,也可以通过tmux命令的一组子命令来与tmux进行交互。事实上,如果研究tmux的配置文件,可以看到其中重要的一项内容就是将快捷键绑定到相应的子命令上。 + +- 前缀(prefix table)快捷键与根(root table)快捷键 + - 许多CLI应用程序都会定义自己的快捷键。作为更底层的程序,tmux需要尽可能避免自己的快 + - 捷键和上层应用程序的快捷键冲突。前缀键就是为了解决这个问题引入的。 + - 前缀快捷键:需要先按下前缀键(默认是 Ctrl+b ),然后再按下相应的快捷键。 + - Root快捷键:直接按下对应的快捷键,不需要前缀键。这些通常是一些不与通常使用的终端快捷键冲突的键。 + +- 命令行模式 + - `` : ,可以进入tmux的内部命令行。在该命令行中可以直接输入tmux子命令(而不是 tmux `<子命令>` ),可以用于使用未绑定快捷键的命令。 + +- pane 管理 + - 切换pane: `` 方向键,tmux命令版本是 select-pane -[UDLR] ,U代表上,D代表下,L代表左,R代表右。同时只能有一个pane接收输入,利用这个命令可以切换当前接收输入的pane。分割当前pane,创建两个新的pane: `"` (上下分割), `%` (左右分割),tmux命令版本是 split-window (默认是上下分割), split-window -h (左右分割)。在初始情况下,每个window只有一个占满整个window的pane,使用这个命令可以分割当前接收输入的pane,创建两个pane,类似于GUI下的分屏功能。 + - 关闭pane: `` x ,然后按 y 确认。tmux命令版本是 kill-pane 。 + +- window管理 + - 创建Window: `c` ,tmux命令版本是 new-window 。 + - 切换Window: `n` (下一个Window), ` p` (上一个Window), ` l` (最后一个活跃Window),tmux命令版本是 next-window , previous-window , last-window 。 + - 切换到指定Window: ``窗口序号 ,tmux命令版本是 select-window -t :窗口序号。 + +- session管理(部分命令没有快捷键,因为不是在session内部进行操作) + + - 创建新session: tmux new-session -s mysession , mysession 是新session的名字。 + - 列出所有session: tmux list-sessions 。 + - attach到一个session: tmux attach -t mysession , mysession 是目标session的名字。 + - detach当前session: `d` ,tmux命令版本是 detach 。 + - 终止某个session: tmux kill-session -t mysession , mysession 是你想要终止的session的名字。 + +- 在某些GUI终端模拟器中,还可以通过鼠标与tmux交互,例如可以通过鼠标拖拽pane的边界来调整各个pane的大小,点击window的名字来切换到指定window等。 + +## 拓展阅读 + +- +- +- +- 如果你希望通过tmux提高自己的工作效率,我们强烈建议你编写适合自己使用习惯的tmux配置文件。 diff --git a/Pages/Assets b/Pages/Assets new file mode 120000 index 00000000..f035b885 --- /dev/null +++ b/Pages/Assets @@ -0,0 +1 @@ +../Assets \ No newline at end of file diff --git a/Pages/Contribute.md b/Pages/Contribute.md new file mode 100644 index 00000000..e69de29b diff --git a/Pages/Getting-started.md b/Pages/Getting-started.md new file mode 100644 index 00000000..ff5dfa49 --- /dev/null +++ b/Pages/Getting-started.md @@ -0,0 +1,10 @@ +# 如何开始实验 + +> [!QUESTION] 思考题 +> 思考题为需要在实验报告中书面回答的问题,需要用文字或者是图片来描述 + +> [!CODING] 练习题 +> 练习题需在 ChCore 代码中填空,并在实验报告阐述实现过程,完成即可获得实验大多数的分数。 + +> [!CHALLENGE] 挑战题 +> 挑战题为难度稍高的练习题,作为实验的附加题用于加深你对代码结构以及系统设计的理解。 diff --git a/Pages/Intro.md b/Pages/Intro.md new file mode 120000 index 00000000..32d46ee8 --- /dev/null +++ b/Pages/Intro.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/Pages/Lab0.md b/Pages/Lab0.md new file mode 100644 index 00000000..c82cd827 --- /dev/null +++ b/Pages/Lab0.md @@ -0,0 +1,29 @@ +# Lab0:拆炸弹 + + + +## 简介 + +在实验 0 中,你需要通过阅读汇编代码以及使用调试工具来拆除一个 +二进制炸弹程序。本实验分为两个部分:第一部分介绍拆弹实验的基本知 +识,包括 ARM 汇编语言、QEMU 模拟器、GDB 调试器的使用;第二部分 +需要分析炸弹程序,推测正确的输入来使得炸弹程序能正常退出。 + +> [!WARNING] +> 在完成本实验之前,请务必将你的学号填写在`student-number.txt`当中,否则本lab实验的成绩将计为0分 + +## Makefile 讲解 + +- `make bomb`: 使用student-number.txt提供的学号,生成炸弹,如果您不是上海交通大学的学生可以自行随意填写。 +- `make qemu`: 使用qemu-aarch64二进制模拟运行炸弹 +- `make qemu-gdb`: 使用qemu-aarch64提供的gdb server进行调试 +- `make gdb`: 使用仓库目录自动生成的$(GDB)定义连接到qemu-aarch64的gdb-server进行调试 + +## 评分与提交规则 + +本实验你只需要提交`ans.txt`以及`student-number.txt`即可 + +> [!IMPORTANT] +> 运行 `make grade` 来得到本实验的分数 +> 运行 `make submit` 会在检查student-number.txt内容之后打包必要的提交文件 + diff --git a/Pages/Lab0/defuse.md b/Pages/Lab0/defuse.md new file mode 100644 index 00000000..c4ca7fcc --- /dev/null +++ b/Pages/Lab0/defuse.md @@ -0,0 +1,86 @@ +# 二进制炸弹拆除 + +我们在实验中提供了一个二进制炸弹程序bomb以及它的部分源码bomb.c。在 bomb.c 中,你可以看到一共有 6 个 phase。对每个 phase,bomb程序将从标准中输入中读取一行用户输入作为这一阶段的拆弹密码。若这一密码错误,炸弹程序将异常退出。你的任务是通过 GDB 以及阅读汇编代码,判断怎样的输入可以使得炸弹程序正常通过每个 phase。以下是一次失败尝试的例子: + +``` +[user@localhost lab0] $ make qemu +qemu -aarch64 bomb +Type in your defuse password: +1234 +BOOM !!! + +``` + +> [!TIP] +> 你需要学习gdb、objdump的使用来查看炸弹程序对应的汇编,并通过断点等方法来查看炸弹运行时的状态(寄存器、内存的值等)。以下是使用gdb来查看炸弹运行状态的例子。在这个例子中,我们在main函数的开头打了一个断点,通过continue让程序运行直至遇到我们设置的断点,使用info查看了寄存器中的值,最终通过x查看了x0寄存器中的地址指向的字符串的内容。以下是输入与输出。 + +```console +add symbol table from file "bomb" +(y or n) y +Reading symbols from bomb ... +(gdb) break main +Breakpoint 1 at 0x4006a4 +(gdb) continue +Continuing. +Breakpoint 1, 0x00000000004006a4 in main () +(gdb) disassemble +Dump of assembler code for function main: +0x0000000000400694 <+0>:stp0x0000000000400698 <+4>:mov +x29 , x30 , [sp , # -16]! +x29 , sp +0x000000000040069c <+8>:adrpx0 , 0x464000 +0x00000000004006a0 <+12>:addx0 , x0 , #0x778 +=> 0x00000000004006a4 <+16>:bl0x413b20 +0x00000000004006a8 <+20>:bl0x400b10 +0x00000000004006ac <+24>:bl0x400734 +0x00000000004006b0 <+28>:bl0x400708 +0x00000000004006b4 <+32>:bl0x400b10 +0x00000000004006b8 <+36>:bl0x400760 +0x00000000004006bc <+40>:bl0x400708 +0x00000000004006c0 <+44>:bl0x400b10 +0x00000000004006c4 <+48>:bl0x400788 +0x00000000004006c8 <+52>:bl0x400708 +0x00000000004006cc <+56>:bl0x400b10 +0x00000000004006d0 <+60>:bl0x400800 +0x00000000004006d4 <+64>:bl0x400708 +0x00000000004006d8 <+68>:bl0x400b10 +0x00000000004006dc <+72>:bl0x4009e4 +0x00000000004006e0 <+76>:bl0x400708 +0x00000000004006e4 <+80>:bl0x400b10 +0x00000000004006e8 <+84>:bl0x400ac0 +0x00000000004006ec <+88>:bl0x400708 +0x00000000004006f0 <+92>:adrpx0 , 0x464000 +0x00000000004006f4 <+96>:addx0 , x0 , #0x798 +0x00000000004006f8 <+100>:bl0x413b20 +0x00000000004006fc <+104>:movw0 , #0x0 +0x0000000000400700 <+108>:ldpx29 , x30 , [sp], #16 +0x0000000000400704 <+112>:ret +// #0 +End of assembler dump. +(gdb) info registers x0 +x0 +0x464778 +4605816 +(gdb) x /s 0x464778 +0x464778: +"Type in your defuse password!" + +``` +在破解后续阶段时,为了避免每次都需要输入先前阶段的拆弹密码,你可以通过重定向的方式来让炸弹程序读取文件中的密码: + +```console +[user@localhost lab0] $ make qemu < ans.txt +qemu -aarch64 bomb +Type in your defuse password: +5 phases to go +4 phases to go +3 phases to go +2 phases to go +1 phases to go +0 phases to go +Congrats! You have defused all phases! + +``` + + + diff --git a/Pages/Lab0/instructions.md b/Pages/Lab0/instructions.md new file mode 100644 index 00000000..22247ba3 --- /dev/null +++ b/Pages/Lab0/instructions.md @@ -0,0 +1,58 @@ +# 基本知识 + +> [!INFO] +> 本部分旨在熟悉 ARM 汇编语言,以及使用 QEMU 和 QEMU/GDB调试 + +--- + + + +## 熟悉Aarch64汇编 + +AArch64 是 ARMv8 ISA 的 64 位执行状态。《ARM 指令集参考指 +南》是一个帮助入门 ARM 语法的手册。在 ChCore 实验中,只 +需要在提示下可以理解一些关键汇编和编写简单的汇编代码即可。 + +## 使用 QEMU 运行炸弹程序 + +我们在实验中提供了bomb二进制文件,但该文件只能运行在基于 AArch64 +的 Linux 中。通过 QEMU,我们可以在其他架构上模拟运行。同时,QEMU +可以结合 GDB 进行调试(如打印输出、单步调试等) + +> [!TIP] +> QEMU 不仅可以模拟运行用户态程序,也可以模拟运行在内核态的操作系统。在前一种模式下,QEMU 会模拟运行用户态的汇编代码,同时将系统调用等翻译为对宿主机的调用。在后一种模式下,QEMU 将在虚拟硬件上模拟一整套计算机启动的过程。 + +在lab0目录下,输入以下命令可以在 QEMU 中运行炸弹程序 + +```console +[user@localhost Lab0]$ make qemu + +``` + +炸弹程序的标准输出将会显示在 QEMU 中: + +```console +Type in your defuse password: + +``` + +## QEMU 与 GDB + +在实验中,由于需要在 x86-64 平台上使用 GDB 来调试 AArch64 代 +码,因此使用gdb-multiarch代替了普通的gdb。使用 GDB 调试的原理是, +QEMU 可以启动一个 GDB 远程目标(remote target) +(使用-s或-S参数 +启动),QEMU 会在真正执行镜像中的指令前等待 GDB 客户端的连接。开 +启远程目标之后,可以开启 GDB 进行调试,它会在某个端口上进行监听。 + +打开两个终端,在bomb-lab目录下,输入make qemu-gdb和make gdb命 +令可以分别打开带有 GDB 调试的 QEMU 以及 GDB,在 GDB 中将会看 +到如下的输出: + +```console +... +0x0000000000400540 in ?? () +... +(gdb) + +``` diff --git a/Pages/Lab1.md b/Pages/Lab1.md new file mode 100644 index 00000000..570087b1 --- /dev/null +++ b/Pages/Lab1.md @@ -0,0 +1,23 @@ +# Lab1: 机器启动 + +## 简介 + +本实验作为 ChCore 操作系统课程实验的第一个实验,分为两个部分:第一部分介绍实验所需的基础知识,第二部分熟悉并完成ChCore 内核的启动过程。 +实验面向的硬件平台是树莓派3b+(AArch64)。你可以在QEMU模拟器上完成实验,也可以在树莓派开发板上完成。 + +本实验代码包含了基础的ChCore 微内核操作系统,除了练习题相关的源码以外,其余部分通过二进制格式提供。 +完成本实验的练习题之后,你可以进入 ChCore shell,运行命令或执行程序。 +例如,可以在 shell 中输入 `hello_world.bin` 运行一个简单的用户态程序; +输入`ls` 查看目录内容。 + +```console + ______ __ __ ______ __ __ ______ __ __ +/\ ___\ /\ \_\ \ /\ ___\ /\ \_\ \ /\ ___\ /\ \ /\ \ +\ \ \____ \ \ __ \ \ \___ \ \ \ __ \ \ \ __\ \ \ \____ \ \ \____ + \ \_____\ \ \_\ \_\ \/\_____\ \ \_\ \_\ \ \_____\ \ \_____\ \ \_____\ + \/_____/ \/_/\/_/ \/_____/ \/_/\/_/ \/_____/ \/_____/ \/_____/ + + +Welcome to ChCore shell! +$ +``` diff --git a/Lab1/assets/lab1-pte-1.png b/Pages/Lab1/assets/lab1-pte-1.png similarity index 100% rename from Lab1/assets/lab1-pte-1.png rename to Pages/Lab1/assets/lab1-pte-1.png diff --git a/Lab1/assets/lab1-pte-2.png b/Pages/Lab1/assets/lab1-pte-2.png similarity index 100% rename from Lab1/assets/lab1-pte-2.png rename to Pages/Lab1/assets/lab1-pte-2.png diff --git a/Lab1/assets/lab1-trans.svg b/Pages/Lab1/assets/lab1-trans.svg similarity index 100% rename from Lab1/assets/lab1-trans.svg rename to Pages/Lab1/assets/lab1-trans.svg diff --git a/Pages/Lab1/boot.md b/Pages/Lab1/boot.md new file mode 100644 index 00000000..cc84805b --- /dev/null +++ b/Pages/Lab1/boot.md @@ -0,0 +1,108 @@ +# 内核启动 + + + +## 树莓派启动过程 + +在树莓派 3B+ 真机上,通过 SD 卡启动时,上电后会运行 ROM 中的特定固件,接着加载并运行 SD 卡上的 `bootcode.bin` 和 `start.elf`,后者进而根据 `config.txt` 中的配置,加载指定的 kernel 映像文件(纯 binary 格式,通常名为 `kernel8.img`)到内存的 `0x80000` 位置并跳转到该地址开始执行。 + +而在 QEMU 模拟的 `raspi3b`(旧版 QEMU 为 `raspi3`)机器上,则可以通过 `-kernel` 参数直接指定 ELF 格式的 kernel 映像文件,进而直接启动到 ELF 头部中指定的入口地址,即 `_start` 函数(实际上也在 `0x80000`,因为 ChCore 通过 linker script 强制指定了该函数在 ELF 中的位置,如有兴趣请参考附录)。 + +## 启动 CPU 0 号核 + +`_start` 函数(位于 `kernel/arch/aarch64/boot/raspi3/init/start.S`)是 ChCore 内核启动时执行的第一块代码。由于 QEMU 在模拟机器启动时会同时开启 4 个 CPU 核心,于是 4 个核会同时开始执行 `_start` 函数。而在内核的初始化过程中,我们通常需要首先让其中一个核进入初始化流程,待进行了一些基本的初始化后,再让其他核继续执行。 + +> [!QUESTION] 思考题 1 +> 阅读 `_start` 函数的开头,尝试说明 ChCore 是如何让其中一个核首先进入初始化流程,并让其他核暂停执行的。 + +> [!HINT] +> 可以在 [Arm Architecture Reference Manual](https://documentation-service.arm.com/static/61fbe8f4fa8173727a1b734e) 找到 `mpidr_el1` 等系统寄存器的详细信息。 + +## 切换异常级别 + +AArch64 架构中,特权级被称为异常级别(Exception Level,EL),四个异常级别分别为 EL0、EL1、EL2、EL3,其中 EL3 为最高异常级别,常用于安全监控器(Secure Monitor),EL2 其次,常用于虚拟机监控器(Hypervisor),EL1 是内核常用的异常级别,也就是通常所说的内核态,EL0 是最低异常级别,也就是通常所说的用户态。 + +QEMU `raspi3b` 机器启动时,CPU 异常级别为 EL3,我们需要在启动代码中将异常级别降为 EL1,也就是进入内核态。具体地,这件事是在 `arm64_elX_to_el1` 函数(位于 `kernel/arch/aarch64/boot/raspi3/init/tools.S`)中完成的。 + +为了使 `arm64_elX_to_el1` 函数具有通用性,我们没有直接写死从 EL3 降至 EL1 的逻辑,而是首先判断当前所在的异常级别,并根据当前异常级别的不同,跳转到相应的代码执行。 + +```asm +{{#include ../../Lab1/kernel/arch/aarch64/boot/raspi3/init/tools.S:70:143}} +``` + +> [!CODING] 练习题 2 +> 在 `arm64_elX_to_el1` 函数的 `LAB 1 TODO 1` 处填写一行汇编代码,获取 CPU 当前异常级别。 + +> [!HINT] +> 通过 `CurrentEL` 系统寄存器可获得当前异常级别。通过 GDB 在指令级别单步调试可验证实现是否正确。注意参考文档理解 `CurrentEL` 各个 bits 的[意义](https://developer.arm.com/documentation/ddi0601/2020-12/AArch64-Registers/CurrentEL--Current-Exception-Level)。 + +`eret`指令可用于从高异常级别跳到更低的异常级别,在执行它之前我们需要设置 +设置 `elr_elx`(异常链接寄存器)和 `spsr_elx`(保存的程序状态寄存器),分别控制`eret`执行后的指令地址(PC)和程序状态(包括异常返回后的异常级别)。 + +> [!CODING] 练习题 3 +> 在 `arm64_elX_to_el1` 函数的 `LAB 1 TODO 2` 处填写大约 4 行汇编代码,设置从 EL3 跳转到 EL1 所需的 `elr_el3` 和 `spsr_el3` 寄存器值。 + +> [!HINT] +> `elr_el3` 的正确设置应使得控制流在 `eret` 后从 `arm64_elX_to_el1` 返回到 `_start` 继续执行初始化。 `spsr_el3` 的正确设置应正确屏蔽 DAIF 四类中断,并且将 [SP](https://developer.arm.com/documentation/ddi0500/j/CHDDGJID) 正确设置为 `EL1h`. 在设置好这两个系统寄存器后,不需要立即 `eret`. + +练习完成后,可使用 GDB 跟踪内核代码的执行过程,由于此时不会有任何输出,可通过是否正确从 `arm64_elX_to_el1` 函数返回到 `_start` 来判断代码的正确性。 + +## 跳转到第一行 C 代码 + +降低异常级别到 EL1 后,我们准备从汇编跳转到 C 代码,在此之前我们先设置栈(SP)。因此,`_start` 函数在执行 `arm64_elX_to_el1` 后,即设置内核启动阶段的栈,并跳转到第一个 C 函数 `init_c`。 + +``` +{{#include ../../Lab1/kernel/arch/aarch64/boot/raspi3/init/start.S:23:82}} + +``` + +> [!QUESTION] 思考题 4 +> 说明为什么要在进入 C 函数之前设置启动栈。如果不设置,会发生什么? + +进入 `init_c` 函数后,第一件事首先通过 `clear_bss` 函数清零了 `.bss` 段,该段用于存储未初始化的全局变量和静态变量(具体请参考附录)。 + +> [!QUESTION] 思考题 5 +> 在实验 1 中,其实不调用 `clear_bss` 也不影响内核的执行,请思考不清理 `.bss` 段在之后的何种情况下会导致内核无法工作。 + +## 初始化串口输出 + +到目前为止我们仍然只能通过 GDB 追踪内核的执行过程,而无法看到任何输出,这无疑是对我们写操作系统的积极性的一种打击。因此在 `init_c` 中,我们启用树莓派的 UART 串口,从而能够输出字符。 + +在 `kernel/arch/aarch64/boot/raspi3/peripherals/uart.c` 已经给出了 `early_uart_init` 和 `early_uart_send` 函数,分别用于初始化 UART 和发送单个字符(也就是输出字符)。 + +``` +{{#include ../../Lab1/kernel/arch/aarch64/boot/raspi3/peripherals/uart.c:140:146}} + +``` + +> [!CODING] 练习题6 +> 在 `kernel/arch/aarch64/boot/raspi3/peripherals/uart.c` 中 `LAB 1 TODO 3` 处实现通过 UART 输出字符串的逻辑。 + +> [!SUCCESS] 第一个字符串 +> 恭喜!我们终于在内核中输出了第一个字符串! +> 感兴趣的同学请思考`early_uart_send`究竟是怎么输出字符的。 + +## 启用 MMU + +在内核的启动阶段,还需要配置启动页表(`init_kernel_pt` 函数),并启用 MMU(`el1_mmu_activate` 函数),使可以通过虚拟地址访问内存,从而为之后跳转到高地址作准备(内核通常运行在虚拟地址空间 `0xffffff0000000000` 之后的高地址)。 + +关于配置启动页表的内容由于包含关于页表的细节,将在本实验下一部分实现,目前直接启用 MMU。 + +在 EL1 异常级别启用 MMU 是通过配置系统寄存器 `sctlr_el1` 实现的(Arm Architecture Reference Manual D13.2.118)。具体需要配置的字段主要包括: + +- 是否启用 MMU(`M` 字段) +- 是否启用对齐检查(`A` `SA0` `SA` `nAA` 字段) +- 是否启用指令和数据缓存(`C` `I` 字段) + +> [!CODING] 练习题7 +> 在 `kernel/arch/aarch64/boot/raspi3/init/tools.S` 中 `LAB 1 TODO 4` 处填写一行汇编代码,以启用 MMU。 + +由于没有配置启动页表,在启用 MMU 后,内核会立即发生地址翻译错误(Translation Fault),进而尝试跳转到异常处理函数(Exception Handler), +该异常处理函数的地址为异常向量表基地址(`vbar_el1` 寄存器)加上 `0x200`。 +此时我们没有设置异常向量表(`vbar_el1` 寄存器的值是0),因此执行流会来到 `0x200` 地址,此处的代码为非法指令,会再次触发异常并跳转到 `0x200` 地址。 +使用 GDB 调试,在 GDB 中输入 `continue` 后,待内核输出停止后,按 Ctrl-C,可以观察到内核在 `0x200` 处无限循环。 + +--- + +> [!IMPORTANT] +> 以上为Lab1 Part1 的内容 diff --git a/Pages/Lab1/pte.md b/Pages/Lab1/pte.md new file mode 100644 index 00000000..83d72b3d --- /dev/null +++ b/Pages/Lab1/pte.md @@ -0,0 +1,99 @@ +# 页表映射 + + + +## AArch64 地址翻译 + +在配置内核启动页表前,我们首先回顾实验涉及到的体系结构知识。这部分内容课堂上已经学习过,如果你已熟练掌握则可以直接跳过这里的介绍(但不要跳过思考题)。 + +在 AArch64 架构的 EL1 异常级别存在两个页表基址寄存器:`ttbr0_el1`[^ttbr0_el1] 和 `ttbr1_el1`[^ttbr1_el1],分别用作虚拟地址空间低地址和高地址的翻译。那么什么地址范围称为“低地址”,什么地址范围称为“高地址”呢?这由 `tcr_el1` 翻译控制寄存器[^tcr_el1]控制,该寄存器提供了丰富的可配置性,可决定 64 位虚拟地址的高多少位为 `0` 时,使用 `ttbr0_el1` 指向的页表进行翻译,高多少位为 `1` 时,使用 `ttbr1_el1` 指向的页表进行翻译[^ttbr-sel]。一般情况下,我们会将 `tcr_el1` 配置为高低地址各有 48 位的地址范围,即,`0x0000_0000_0000_0000`~`0x0000_ffff_ffff_ffff` 为低地址,`0xffff_0000_0000_0000`~`0xffff_ffff_ffff_ffff` 为高地址。 + +[^ttbr0_el1]: Arm Architecture Reference Manual, D13.2.144 +[^ttbr1_el1]: Arm Architecture Reference Manual, D13.2.147 +[^tcr_el1]: Arm Architecture Reference Manual, D13.2.131 +[^ttbr-sel]: Arm Architecture Reference Manual, D5.2 Figure D5-13 + +了解了如何决定使用 `ttbr0_el1` 还是 `ttbr1_el1` 指向的页表,再来看地址翻译过程如何进行。通常我们会将系统配置为使用 4KB 翻译粒度、4 级页表(L0 到 L3),同时在 L1 和 L2 页表中分别允许映射 2MB 和 1GB 大页(或称为块)[^huge-page],因此地址翻译的过程如下图所示: + +[^huge-page]: 操作系统:原理与实现 + +![](assets/lab1-trans.svg) + +其中,当映射为 1GB 块或 2MB 块时,图中 L2、L3 索引或 L3 索引的位置和低 12 位共同组成块内偏移。 + +每一级的每一个页表占用一个 4KB 物理页,称为页表页(Page Table Page),其中有 512 个条目,每个条目占 64 位。AArch64 中,页表条目称为描述符(descriptor)[^descriptor],最低位(bit[0])为 `1` 时,描述符有效,否则无效。有效描述符有两种类型,一种指向下一级页表(称为表描述符),另一种指向物理块(大页)或物理页(称为块描述符或页描述符)。在上面所说的地址翻译配置下,描述符结构如下(“Output address”在这里即物理地址,一些地方称为物理页帧号(Page Frame Number,PFN)): + +**L0、L1、L2 页表描述符** + +![](assets/lab1-pte-1.png) + +**L3 页表描述符** + +![](assets/lab1-pte-2.png) + +[^descriptor]: Arm Architecture Reference Manual, D5.3 + +> [!QUESTION]思考题 8 +> 请思考多级页表相比单级页表带来的优势和劣势(如果有的话),并计算在 AArch64 页表中分别以 4KB 粒度和 2MB 粒度映射 0~4GB 地址范围所需的物理内存大小(或页表页数量)。 + +页表描述符中除了包含下一级页表或物理页/块的地址,还包含对内存访问进行控制的属性(attribute)。这里涉及到太多细节,本文档限于篇幅只介绍最常用的几个页/块描述符中的属性字段: + +字段 | 位 | 描述 +--- | --- | --- +UXN | bit[54] | 置为 `1` 表示非特权态无法执行(Unprivileged eXecute-Never) +PXN | bit[53] | 置为 `1` 表示特权态无法执行(Privileged eXecute-Never) +nG | bit[11] | 置为 `1` 表示该描述符在 TLB 中的缓存只对当前 ASID 有效 +AF | bit[10] | 置为 `1` 表示该页/块在上一次 AF 置 `0` 后被访问过 +SH | bits[9:8] | 表示可共享属性[^mem-attr] +AP | bits[7:6] | 表示读写等数据访问权限[^mem-access] +AttrIndx | bits[4:2] | 表示内存属性索引,间接指向 `mair_el1` 寄存器中配置的属性[^mair_el1],用于控制将物理页映射为正常内存(normal memory)或设备内存(device memory),以及控制 cache 策略等 + +[^mem-attr]: Arm Architecture Reference Manual, D5.5 +[^mem-access]: Arm Architecture Reference Manual, D5.4 +[^mair_el1]: Arm Architecture Reference Manual, D13.2.97 + +## 配置内核启动页表 + +有了关于页表配置的前置知识,我们终于可以开始配置内核的启动页表了。 + +操作系统内核通常运行在虚拟内存的高地址(如前所述,`0xffff_0000_0000_0000` 之后的虚拟地址)。通过对内核页表的配置,将虚拟内存高地址映射到内核实际所在的物理内存,在执行内核代码时,PC 寄存器的值是高地址,对全局变量、栈等的访问都使用高地址。在内核运行时,除了需要访问内核代码和数据等,往往还需要能够对任意物理内存和外设内存(MMIO)进行读写,这种读写同样通过高地址进行。 + +因此,在内核启动时,首先需要对内核自身、其余可用物理内存和外设内存进行虚拟地址映射,最简单的映射方式是一对一的映射,即将虚拟地址 `0xffff_0000_0000_0000 + addr` 映射到 `addr`。需要注意的是,在 ChCore 实验中我们使用了 `0xffff_ff00_0000_0000` 作为内核虚拟地址的开始(注意开头 `f` 数量的区别),不过这不影响我们对知识点的理解。 + +在树莓派 3B+ 机器上,物理地址空间分布如下[^bcm2836]: + +[^bcm2836]: [bcm2836-peripherals.pdf](https://datasheets.raspberrypi.com/bcm2836/bcm2836-peripherals.pdf) & [Raspberry Pi Hardware - Peripheral Addresses](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#peripheral-addresses) + +物理地址范围 | 对应设备 +--- | --- +`0x00000000`~`0x3f000000` | 物理内存(SDRAM) +`0x3f000000`~`0x40000000` | 共享外设内存 +`0x40000000`~`0xffffffff` | 本地(每个 CPU 核独立)外设内存 + +现在将目光转移到 `kernel/arch/aarch64/boot/raspi3/init/mmu.c` 文件,我们需要在 `init_kernel_pt` 为内核配置从 `0x00000000` 到 `0x80000000`(`0x40000000` 后的 1G,ChCore 只需使用这部分地址中的本地外设)的映射,其中 `0x00000000` 到 `0x3f000000` 映射为 normal memory,`0x3f000000` 到 `0x80000000`映射为 device memory,其中 `0x00000000` 到 `0x40000000` 以 2MB 块粒度映射,`0x40000000` 到 `0x80000000` 以 1GB 块粒度映射。 + +> [!QUESTION] 思考题 9 +> 请结合上述地址翻译规则,计算在练习题 10 中,你需要映射几个 L2 页表条目,几个 L1 页表条目,几个 L0 页表条目。页表页需要占用多少物理内存? + +> [!CODING] 练习题 10 +> 在 `init_kernel_pt` 函数的 `LAB 1 TODO 5` 处配置内核高地址页表(`boot_ttbr1_l0`、`boot_ttbr1_l1` 和 `boot_ttbr1_l2`),以 2MB 粒度映射。 + +> [!HINT] +> 你只需要将 `addr`(`0x00000000` 到 `0x80000000`) 按照要求的页粒度一一映射到 `KERNEL_VADDR + addr`(`vaddr`) 上。`vaddr` 对应的物理地址是 `vaddr - KERNEL_VADDR`. Attributes 的设置请参考给出的低地址页表配置。 + +> [!QUESTION] 思考题11 +> 请思考在 `init_kernel_pt` 函数中为什么还要为低地址配置页表,并尝试验证自己的解释。 + +完成 `init_kernel_pt` 函数后,ChCore 内核便可以在 `el1_mmu_activate` 中将 `boot_ttbr1_l0` 等物理地址写入实际寄存器(如 `ttbr1_el1` ),随后启用 MMU 后继续执行,并通过 `start_kernel` 跳转到高地址,进而跳转到内核的 `main` 函数(位于 `kernel/arch/aarch64/main.c`, 尚未发布,以 binary 提供)。 + +> [!QUESTION] 思考题12 +> 在一开始我们暂停了三个其他核心的执行,根据现有代码简要说明它们什么时候会恢复执行。思考为什么一开始只让 0 号核心执行初始化流程? + +> [!HINT] +> `secondary_boot_flag` 将在 main 函数执行完时钟,调度器,锁的初始化后被设置。 + +--- + +> [!SUCCESS] +> 以上为Lab1 Part2 的内容 +> 如果顺利的话 运行make grade你会得到100/100 diff --git a/Pages/SUMMARY.md b/Pages/SUMMARY.md new file mode 100644 index 00000000..6fac81ec --- /dev/null +++ b/Pages/SUMMARY.md @@ -0,0 +1,28 @@ +# Summary + +[前言](./Intro.md) +[如何开始实验](./Getting-started.md) +[贡献指南]() + +- [Lab0:拆炸弹](./Lab0.md) + - [基本知识](./Lab0/instructions.md) + - [二进制炸弹拆除](./Lab0/defuse.md) + +- [Lab1: 机器启动](./Lab1.md) + - [内核启动](./Lab1/boot.md) + - [页表映射](./Lab1/pte.md) + +- [附录](./Appendix.md) + - [Lab0: 工具教程](./Appendix/toolchains.md) + - [TL;DR Cheatsheet](./Appendix/toolchains/tldr.md) + - [tmux](./Appendix/toolchains/tmux.md) + - [gdb](./Appendix/toolchains/gdb.md) + - [源码级调试 vs 汇编级调试](./Appendix/toolchains/gdb/comparison.md) + - [使用简介与扩展阅读](./Appendix/toolchains/gdb/usage.md) + - [objdump](./Appendix/toolchains/objdump.md) + - [make](./Appendix/toolchains/make.md) + - [qemu](./Appendix/toolchains/qemu.md) + - [进程级模拟 vs 系统级模拟](./Appendix/toolchains/qemu/emulation.md) + - [GDBServer](./Appendix/toolchains/qemu/usage.md) + - [Lab1: ELF格式](./Appendix/elf.md) + - [Lab1: Linker Script](./Appendix/linker.md) diff --git a/README.md b/README.md index 2d3e0c49..3cd65be3 100644 --- a/README.md +++ b/README.md @@ -7,46 +7,47 @@ 课程教材: -The course textbook - -**如果你有任何建议或更正意见,欢迎提交 Pull Requests 或 Issues。让我们一起合作改进实验~** +The course textbook +> [!NOTE] +> 如果你有任何建议或更正意见,欢迎提交 Pull Requests 或 Issues。让我们一起合作改进实验 ## Lab0: 拆炸弹(ARM汇编) + 该实验受到CSAPP课程启发,CSAPP课程设计了一个针对x86/x86-64汇编的拆炸弹实验。 不同之处在于,本实验目标是熟悉ARM汇编语言,并为后续的ARM/树莓派内核实验做好准备。 -Tutorial: https://www.bilibili.com/video/BV1q94y1a7BF/?vd_source=63231f40c83c4d292b2a881fda478960 +Tutorial: ## Lab1: 内核启动 + 该实验的主要内容是关于如何在内核启动过程中设置CPU异常级别、配置内核页表并启用MMU。 在内核实验系列中,我们将使用 [ChCore 微内核](https://www.usenix.org/conference/atc20/presentation/gu) 的基础版本,并使用 Raspi3b+作为实验平台(无论是使用QEMU树莓派模拟器还是树莓派开发板都可以)。 -Tutorial: [https://www.bilibili.com/video/BV1gj411i7dh/](https://www.bilibili.com/video/BV1gj411i7dh/?spm_id_from=333.337.search-card.all.click) - +Tutorial: ## Lab2: 内存管理 -该实验主要内容是关于内核中的伙伴系统和slab分配器的实现,并为应用程序设置页表。 -Tutorial: https://www.bilibili.com/video/BV1284y1Q7Jc/?vd_source=316867e8ad2c56f50fa94e8122dd7d38 +该实验主要内容是关于内核中的伙伴系统和slab分配器的实现,并为应用程序设置页表。 +Tutorial: ## Lab3: 进程与线程 -该实验主要内容包括创建第一个用户态进程和线程,完善异常处理流程和系统调用,编写一个Hello-World在实验内核上运行。 -Tutorial: https://www.bilibili.com/video/BV11N411j7bR/ +该实验主要内容包括创建第一个用户态进程和线程,完善异常处理流程和系统调用,编写一个Hello-World在实验内核上运行。 +Tutorial: ## Lab4:多核调度与IPC -该实验中可以看到多核是如何启动的、多线程如何调度、基于capability权限管控的进程间通信机制。 -Tutorial: https://www.bilibili.com/video/BV1AS421N7rU/ +该实验中可以看到多核是如何启动的、多线程如何调度、基于capability权限管控的进程间通信机制。 +Tutorial: ## Lab5:虚拟文件系统 -该实验关注虚拟文件系统(Virtual File System,VFS), VFS抽象层使得不同类型的文件系统可以在应用程序层面以统一的方式进行访问。 +该实验关注虚拟文件系统(Virtual File System,VFS), VFS抽象层使得不同类型的文件系统可以在应用程序层面以统一的方式进行访问。 ## Lab6:GUI -该实验将详细介绍ChCore上基于Wayland的GUI系统的运行原理,包括Wayland通信协议和Wayland Compositor,并且要求读者在了解基于Wayland的GUI系统运行原理的基础上,基于ChCore的GUI框架编写自己的具有GUI界面的APP。 +该实验将详细介绍ChCore上基于Wayland的GUI系统的运行原理,包括Wayland通信协议和Wayland Compositor,并且要求读者在了解基于Wayland的GUI系统运行原理的基础上,基于ChCore的GUI框架编写自己的具有GUI界面的APP。 diff --git a/scripts/.gitignore b/Scripts/.gitignore similarity index 100% rename from scripts/.gitignore rename to Scripts/.gitignore diff --git a/scripts/env_setup.sh b/Scripts/env_setup.sh similarity index 100% rename from scripts/env_setup.sh rename to Scripts/env_setup.sh diff --git a/Scripts/extras.mk b/Scripts/extras.mk new file mode 100644 index 00000000..e69de29b diff --git a/scripts/fixdeps.sh b/Scripts/fixdeps.sh similarity index 100% rename from scripts/fixdeps.sh rename to Scripts/fixdeps.sh diff --git a/Scripts/grader.sh b/Scripts/grader.sh new file mode 100755 index 00000000..908c50c0 --- /dev/null +++ b/Scripts/grader.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +if [[ -z $PROJECT ]]; then + echo "Please set the PROJECT environment variable to the root directory of your project. (Makefile)" + exit 1 +fi + +. ${PROJECT}/Scripts/env_setup.sh + +make="${MAKE:-make}" +info "Grading lab ${LAB} ...(may take 10 seconds)" + +bold "====================" +${PROJECT}/Lab${LAB}/grade.exp +score=$? +info "Score: $score/100" +bold "====================" + +if [[ $score -gt 100 ]]; then + fatal "Score is greater than 100, something went wrong." +fi + +if [[ ! $score -eq 100 ]]; then + exit 1 +else + exit 0 +fi diff --git a/Scripts/kernel.mk b/Scripts/kernel.mk new file mode 100644 index 00000000..0baaa4f1 --- /dev/null +++ b/Scripts/kernel.mk @@ -0,0 +1,47 @@ +V ?= 0 +Q := @ + +ifeq ($(V), 1) + Q := +endif + +BUILD_DIR := $(CURDIR)/build +KERNEL_IMG := $(BUILD_DIR)/kernel.img +QEMU := qemu-system-aarch64 +_QEMU := $(CURDIR)/scripts/qemu/qemu_wrapper.sh $(QEMU) +QEMU_GDB_PORT := 1234 +QEMU_OPTS := -machine raspi3b -nographic -serial mon:stdio -m size=1G -kernel $(KERNEL_IMG) +CHBUILD := $(CURDIR)/chbuild + +export PROJECT CURDIR LAB + +all: build + +defconfig: + $(Q)$(CHBUILD) defconfig + +build: + $(Q)test -f $(CURDIR)/.config || $(CHBUILD) defconfig + $(Q)$(CHBUILD) build + +clean: + $(Q)$(CHBUILD) clean + +distclean: + $(Q)$(CHBUILD) distclean + +qemu: + $(Q)$(_QEMU) $(QEMU_OPTS) + +qemu-gdb: + $(Q)$(_QEMU) -S -gdb tcp::$(QEMU_GDB_PORT) $(QEMU_OPTS) + +gdb: + $(Q)$(GDB) --nx -x $(CURDIR)/.gdbinit + +grade: + $(Q)test -f $(CURDIR)/.config && cp $(CURDIR)/.config $(CURDIR)/.config.bak + $(Q)$(PROJECT)/Scripts/grader.sh + $(Q)test -f $(CURDIR)/.config.bak && mv $(CURDIR)/.config.bak $(CURDIR)/.config + +.PHONY: qemu qemu-gdb gdb defconfig build clean distclean grade all diff --git a/Scripts/lab.mk b/Scripts/lab.mk new file mode 100644 index 00000000..07d5a1d7 --- /dev/null +++ b/Scripts/lab.mk @@ -0,0 +1,26 @@ +# Note that this file should be included directly in every Makefile inside each lab's folder. +# This sets up the environment variable for lab's Makefile. + +ifndef PROJECT +PROJECT := $(shell git rev-parse --show-toplevel) +endif + +# Toolchain configuration +GDB ?= gdb + +ifeq (,$(wildcard $(PROJECT)/Scripts/env_generated.mk)) + $(error Please run fixdeps to create the environment first!) +endif + +ifeq (,$(LAB)) +$(error LAB is not set!) +endif + +ifeq ($(shell test $(LAB) -eq 0; echo $$?),1) + ifeq ($(shell test $(LAB) -gt 5; echo $$?),0) + include $(PROJECT)/Scripts/extras.mk + else + include $(PROJECT)/Scripts/kernel.mk + endif + include $(PROJECT)/Scripts/submit.mk +endif diff --git a/Scripts/submit.mk b/Scripts/submit.mk new file mode 100644 index 00000000..cef69846 --- /dev/null +++ b/Scripts/submit.mk @@ -0,0 +1,7 @@ +include $(CURDIR)/filelist.mk + +submit: + $(Q)tar -czf lab$(LAB).tar.gz $(FILES) + $(Q)echo " Submit Lab$(LAB)" + +.PHONY: submit diff --git a/book.toml b/book.toml new file mode 100644 index 00000000..a1652964 --- /dev/null +++ b/book.toml @@ -0,0 +1,18 @@ +[book] +authors = ["Institute of Parallel and Distributed Systems, Shanghai Jiao Tong University"] +language = "zh-CN" +multilingual = false +src = "Pages" +title = "IPADS OS Course Lab Manual" + +[output.html] +additional-js = ["mermaid.min.js", "mermaid-init.js"] +git-repository-url = "https://github.com/SJTU-IPADS/OS-Course-Lab" +git-repository-icon = "fa-github" + +[preprocessor.callouts] +[preprocessor.mermaid] +[preprocessor.toc] +render = "html" +[preprocessor.last-changed] +render = "html" diff --git a/scripts/lab.mk b/scripts/lab.mk deleted file mode 100644 index b5f306fe..00000000 --- a/scripts/lab.mk +++ /dev/null @@ -1,13 +0,0 @@ -# Note that this file should be included directly in every Makefile inside each lab's folder. -# This sets up the environment variable for lab's Makefile. - -ifndef PROJECT -PROJECT:=(shell git rev-parse --show-toplevel) -endif - -# Toolchain configuration -GDB?=gdb - -ifeq (,$(wildcard $(PROJECT)/scripts/env_generated.mk)) -$(error Please run fixdeps to create the environment first!) -endif