0%

rCore_Tutorial_CP1

rCore_Tutorial_CP1

第一章:应用程序与基础环境

gdb调试指令

一些可能用到的 gdb 指令:

x/10i 0x80000000 : 显示 0x80000000 处的10条汇编指令。
x/10i $pc : 显示即将执行的10条汇编指令。
x/10xw 0x80000000 : 显示 0x80000000 处的10条数据,格式为16进制32bit。
info register: 显示当前所有寄存器信息。
info r t0: 显示 t0 寄存器的值。
break funcname: 在目标函数第一条指令处设置断点。
break *0x80200000: 在 0x80200000 处设置断点。
continue: 执行直到碰到断点。
si: 单步执行一条汇编指令。

内核第一条指令(实践篇)

链接布局

可以手写链接器规则来定义程序的内存布局,此处将text段放置在固定位置0x80200000
文中有一个函数ALIGN没提到什么意思,不过看那个样子也能猜出来,应该是内存布局对其到4k
BLOCK and ALIGN in linker script

理论上来说这里规划的内存布局就是程序被加载到内存后各段的布局。但是这里仍然执行了一步裁剪,将程序只保留了text段的一点点汇编。这个的主要原因是qemu加载文件的时候直接把整个文件装载到对应地址了,并没有按照文件头提供的信息进行装载,故等于我们手动把代码装载上去(说起来这种删去了文件各种信息的纯二进制的指令实际上就和shellcode是一个概念吧,不过shellcode应该位置无关,这个位置有关还得装载到指定地址)

有一个指令readelf -ahW可以高强度查看elf文件布局,就可以看各个段的长度位置之类的
qemu退出为Ctrl a后按x,记得松开ctrl再按x

这章有提到0x1000处的qemu的加载指令,以及rustsbi提供的加载指令。暂且认为rustsbi就等同于BIOS里的BootLoader吧,起码BootLoader这块不需要我自己实现了

为内核支持函数调用

手搓汇编啊。。。等于说要自己实现libc提供的main函数装载环节了

调用规范

这句话说的很好,有一种我不好用语言表达他却说明白了的感觉

调用规范是对于一种确定的编程语言来说的,因为一般意义上的函数调用只会在编程语言的内部进行。

但是这里说到栈内数据访问是通过栈顶指针通过加法向上访问的,感觉许多情况确实是这样,但是总感觉栈是从栈底往上生长的,从栈底做减法往下不应该会更合理吗?不然遇到局部变量栈生长的时候栈顶位置就会发生变化?或者说编译器在一开始就把所有的局部变量提升并规定好了长度?

局部变量估计是被提升到最上面一次性入栈了?函数参数是从后往前压入栈的。。。所以从栈底往上加确实能顺序访问到各个参数,多套一层大括号如for循环之类的似乎会导致栈又生长一次

还提到了caller和callee间寄存器的保存于恢复,之前学32位栈结构的时候好像从来没考虑寄存器。返回地址,栈帧地址,局部变量好像就是栈的全部了

分配并使用启动栈

手动分配初始栈位置,手写汇编定义了一个.bss.stack

    .section .bss.stack
    .globl boot_stack
boot_stack:
    .space 4096 * 16
    .globl boot_stack_top
boot_stack_top:

并将其塞入真实的bss段下方

.bss : {
    *(.bss.stack)
    sbss = .;
    *(.bss .bss.*)
    *(.sbss .sbss.*)
}
ebss = .;

之后手动将栈底指向这个自定义的.bss.stack段。。。感觉其他的可执行程序应该不会是这么实现的吧?可能是直接依赖glibc在程序装载时给他分配栈空间和初始栈帧
这里手动分配占空间应该是因为我们自己就是最底层的环境,没有lib来给我安排初始化栈了,但是为什么不自定义一个其他的section放在高位地址而是整了个bss段下的小段并塞在bss段下面呢。。。

所以我想魔改一下asm直接在bss段后面加一个.mystack段,再改一下linker把这个段链接到bss段后面,感觉会合理一点,然后链接大失败。。。果然我还是完全没有理解这个过程。。。

这段的操作只是初始化栈的位置和指定栈底,函数调用的出入栈保存恢复都由编译器搞定了。。。不然这个手写汇编可能要暴毙

然后是这段清理bss段代码

fn clear_bss() {
    extern "C" {
        fn sbss();
        fn ebss();
    }
    (sbss as usize..ebss as usize).for_each(|a| {
        unsafe { (a as *mut u8).write_volatile(0) }
    });
}

extern C可以使用C声明的外部函数,但实际上函数也就是一段机器指令,函数的符号也不过是一个指向函数实现的指针。这两个是在linker里面定义的bss段的起始地址和结束地址symbol,把它当函数引入,实际上是引用位置标志并将其转成 usize 获取它的地址。(说实话还是有点玄幻)然后再一波把地址转成*mut u8的指向单个字节的指针,把指向的地址写为0。评论区里面有一个小朋友认为*mut u8是解引用,显然是C基础没学好

基于 SBI 服务完成输出和关机

sbi除了BootLoader功能外,还是多多少少又套了点其他的功能,起码不用手写驱动了。。。
可以通过与SBI交互完成简单的IO开关机之类的功能

但是这里有一段代码有点超越rust垃圾的理解范围

实现格式化输出

我直接看菜鸟教程
Rust 组织管理
Rust 泛型与特性

use crate::sbi::console_putchar;
use core::fmt::{self, Write};

struct Stdout;

impl Write for Stdout {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        for c in s.chars() {
            console_putchar(c as usize);
        }
        Ok(())
    }
}

core::fmt::Write是一个rust定义的trait(特性),特性类似于接口,但是特性本身可以实现方法,使得impl特性的结构体可以调用其default的方法。Stdout是我们定义的一个没有成员的结构体,直接去翻Write定义,其存在三个方法

fn write_str(&mut self, s: &str) -> Result
fn write_char(&mut self, c: char) -> Result
fn write_fmt(mut self: &mut Self, args: Arguments<'_>) -> Result

其中write_str没有实现,所以这里手动实现了一下write_str,但目的却在于调用现成的write_fmt方法

然后下面是两个超级rust宏定义,定义了经典print和println两个宏,但这个定义看不懂。。。

需要把如下代码放在其他引入的mod的前面,这样才能在其他引入的mod中使用这里面定义的宏

#[macro_use]
mod console;

然后就可以用那个究极命令一把梭运行了

qemu-system-riscv64 \
-machine virt \
-nographic \
-bios ../bootloader/rustsbi-qemu.bin \
-device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000

然后发现卡住了,然后发现新编译出来的东西没有strip header。。。
删一下rust-objcopy --strip-all target/riscv64gc-unknown-none-elf/release/os -O binary target/riscv64gc-unknown-none-elf/release/os.bin
删之前可以再用readelf看一眼加了rust编译出来的文件,段内容就拉满了,加了一堆rust的符号,不过text段起始还是经典入口0x80200000(linker里面写好的,必然如此)

成功运行,任务完成

基于 GDB 验证启动流程

写完了代码再反过来调
这里用教程给出的riscv toolchain py版本debug,但是pwndbg和教程给出的dashboard都用不了,报错是so里面有一个symbol找不到,不会修拉倒,开始高强度手调

用给出的命令调试

riscv64-unknown-elf-gdb \
    -ex 'file target/riscv64gc-unknown-none-elf/release/os' \
    -ex 'set arch riscv:rv64' \
    -ex 'target remote localhost:1234'

这里的file参数感觉没什么用?有用,虽然file不写也能跑,但是指定之后能读符号表,就会变的友好一点
调试进去之后0x1000这个纯机器码环节。。。常见的step next disassemble全用不了。。。只能用这里给出的si单步汇编,以及x/10i $pc打印PC寄存器后十条指令

看汇编也太折磨我了。。。总之就是一通操作把t0写成了目标地址jump过去了,然后跳过0x80000000的rustsbi部分,直接在地址上下断点,快进到BootLoader,下断点快进到自己手写的entry

(gdb) b *0x80200000
Breakpoint 1 at 0x80200000
(gdb) c
Continuing.

Breakpoint 1, 0x0000000080200000 in stext ()
(gdb) disassemble 
Dump of assembler code for function stext:
=> 0x0000000080200000 <+0>:    auipc    sp,0x14
   0x0000000080200004 <+4>:    mv    sp,sp
   0x0000000080200008 <+8>:    auipc    ra,0x0
   0x000000008020000c <+12>:    jalr    470(ra) # 0x802001de <os::rust_main>
End of assembler dump.
    la sp, boot_stack_top
    call rust_main

可以看到当初手写的两条汇编被翻译成了四条指令,有了符号表jump到rust main之后甚至能看对应源码
说起来,这里似乎将stext这个标记对应的这段汇编当成函数了,不过现在栈顶寄存器的值sp还是0x0,毕竟这段汇编是用来设置栈顶的嘛

然后后面猛调一通也看不懂哈哈
可以看下这个RISC-V函数调用规范

练习

编程第一题列目录网上复制粘贴一下就行,第二题太难了8不会,第三题系统调用???我自己不是操作系统吗,还能调用谁,rustsbi里面好像也没有看到对应的调用

做一下那个log输出和颜色吧。这个就需要去定义宏。。。就得看之前那段抽象代码并且模仿一下了
RUST宏
Rust宏开发入门
得出结论rust真反人类啊。虽然还是不会写,但是就着现在手头上已有的println魔改加颜色还是能做到的,concat前后加上颜色代码即可

/// info string macro
#[macro_export]
macro_rules! info {
    ($fmt: literal $(, $($arg: tt)+)?) => {
        $crate::console::print(format_args!(concat!("\x1b[34m", $fmt, "\x1b[0m\n") $(, $($arg)+)?));
    }
}

log输出等级控制完全不会

请用相关工具软件分析并给出应用程序A的代码段/数据段/堆/栈的地址空间范围。

不会,只会把程序用gdb挂住然后翻pid去翻proc maps。。。

为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议?

寄存器有哪些?但是操作系统也不能决定寄存器有几个吧?从能用就行的角度来看,感觉操作系统的作用就是提供一套lib捏。那lib里的东西,感觉就是可执行文件格式,系统调用之类的吧。调用规范反正是编译器自己定,内存分配之类的对程序本身也透明,就程序加载进来的时候得按照操作系统定义的格式加载之类的

为何应用程序员编写应用时不需要建立栈空间和指定地址空间?

因为操作系统,我滴超人。

简而言之,系统会为你设置栈,并且将argc,argv和envp压入栈中。文件描述符0,1和2(stdin, stdout和stderr)保留shell之前的设置。加载器会帮你完成重定位,调用你设置的预初始化函数。当所有搞定之后,控制权会传递给_start()
Linux X86 程序启动 – main函数是如何被执行的?