修改vb6的编译器c2.exe使它可以输出汇编代码_xv6笔记-启动代码分析

首先看xv6 commit的第一个makefile

OBJS = main.o 

CC = i386-jos-elf-gcc
LD = i386-jos-elf-ld
OBJCOPY = i386-jos-elf-objcopy
OBJDUMP = i386-jos-elf-objdump

xv6.img : bootblock kernel
    dd if=/dev/zero of=xv6.img count=10000
    dd if=bootblock of=xv6.img conv=notrunc
    dd if=kernel of=xv6.img seek=1 conv=notrunc

bootblock : bootasm.S bootmain.c
    $(CC) -O -nostdinc -I. -c bootmain.c
    $(CC) -nostdinc -I. -c bootasm.S
    $(LD) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o bootmain.o
    $(OBJDUMP) -S bootblock.o > bootblock.asm
    $(OBJCOPY) -S -O binary bootblock.o bootblock
    ./sign.pl bootblock

kernel : $(OBJS)
    $(LD) -Ttext 0x100000 -e main -o kernel $(OBJS)
    $(OBJDUMP) -S kernel > kernel.asm

%.o: %.c
    $(CC) -nostdinc -I. -O -c -o $@ $<

clean : 
    rm -f bootmain.o bootasm.o bootblock.o bootblock
    rm -f kernel main.o kernel.asm xv6.img

修改后的简化版支持qemu和bochs的Makefile

#1.编译目标定义
OBJS = main.o
#只要make查到某个 .o 文件,它就自动把相关的 .c 加到依赖关系中



#2.配置编译工具gcc
ifndef TOOLPREFIX
TOOLPREFIX := $(shell if i386-jos-elf-objdump -i 2>&1 | grep '^elf32-i386$$' >/dev/null 2>&1; 
    then echo 'i386-jos-elf-'; 
    elif objdump -i 2>&1 | grep 'elf32-i386' >/dev/null 2>&1; 
    then echo ''; 
    else echo "***" 1>&2; 
    echo "*** Error: Couldn't find an i386-*-elf version of GCC/binutils." 1>&2; 
    echo "*** Is the directory with i386-jos-elf-gcc in your PATH?" 1>&2; 
    echo "*** If your i386-*-elf toolchain is installed with a command" 1>&2; 
    echo "*** prefix other than 'i386-jos-elf-', set your TOOLPREFIX" 1>&2; 
    echo "*** environment variable to that prefix and run 'make' again." 1>&2; 
    echo "*** To turn off this error, run 'gmake TOOLPREFIX= ...'." 1>&2; 
    echo "***" 1>&2; exit 1; fi)
endif
#若找不到合适的版本,输出错误信息



#3.配置模拟器qemu
ifndef QEMU
QEMU = $(shell if which qemu > /dev/null; 
    then echo qemu; exit; 
    elif which qemu-system-i386 > /dev/null; 
    then echo qemu-system-i386; exit; 
    elif which qemu-system-x86_64 > /dev/null; 
    then echo qemu-system-x86_64; exit; 
    else 
    qemu=/Applications/Q.app/Contents/MacOS/i386-softmmu.app/Contents/MacOS/i386-softmmu; 
    if test -x $$qemu; then echo $$qemu; exit; fi; fi; 
    echo "***" 1>&2; 
    echo "*** Error: Couldn't find a working QEMU executable." 1>&2; 
    echo "*** Is the directory containing the qemu binary in your PATH" 1>&2; 
    echo "*** or have you tried setting the QEMU variable in Makefile?" 1>&2; 
    echo "***" 1>&2; exit 1)
endif
#检索QEMU路径为变量QEMU赋值,若前面已定义则跳过这一步,若没有可用的QEMU,
#尝试使用i386-softmmu(针对MacOS),否则输出错误信息



#4.配置编译器、汇编器、链接器、copy工具和dump工具(反编译)
CC = $(TOOLPREFIX)gcc
# i386-jos-elf-gcc编译器
AS = $(TOOLPREFIX)gas
# i386-jos-elf-gas汇编器
LD = $(TOOLPREFIX)ld
# i386-jos-elf-ld链接器
# 通过shell指令为CC、AS和LD添加附加参数,指定copy工具和dump工具
OBJCOPY = $(TOOLPREFIX)objcopy
OBJDUMP = $(TOOLPREFIX)objdump
CFLAGS = -fno-pic -static -fno-builtin -fno-strict-aliasing -O2 -Wall -MD -ggdb -m32 -Werror -fno-omit-frame-pointer
# -fno-pic不使用PIC(位置无关代码),-static将依赖的动态库编译为静态,
# -fno-builtin不使用C语言自身的内建函数,因为是要写一个完整的操作系统,防止重名,
# -fno-strict-aliasing编译器规则优化,使一些规则(-O1,-O2,-O3)可以混淆使用。
# -Wall显示警告 -MD编译并保存代码依赖性 -ggdb产生GDB所需的调试信息 -m32生成32位汇编代码(默认64)-Werror遇到警告也停止编译
# -fno-omit-frame-pointer保留函数调用产生的frame pointer,方便调试时的回溯
CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
# -fno-stack-protector 禁用栈保护,使编译器不会对局部变量的组织方式进行重新布局
# -E -x c对后缀c的文件进行预处理而不编译
ASFLAGS = -m32 -gdwarf-2 -Wa,-divide
# FreeBSD ld wants ``elf_i386_fbsd''指定ELF文件系统格式和x86架构
LDFLAGS += -m $(shell $(LD) -V | grep elf_i386 2>/dev/null | head -n 1)
# LDFLAGS即链接到相应版本的模拟器,grep管道查看匹配elf_i386架构的模拟器

# Disable PIE when possible (for Ubuntu 16.10 toolchain) PIE用于将程序装载到随机的地址,这里选择禁用PIE机制
ifneq ($(shell $(CC) -dumpspecs 2>/dev/null | grep -e '[^f]no-pie'),)
CFLAGS += -fno-pie -no-pie
endif
ifneq ($(shell $(CC) -dumpspecs 2>/dev/null | grep -e '[^f]nopie'),)
CFLAGS += -fno-pie -nopie
endif 



#5.生成xv6镜像(即最终装载到模拟器的)
xv6.img: bootblock kernel
    dd if=/dev/zero of=xv6.img count=10000
    dd if=bootblock of=xv6.img conv=notrunc
    dd if=kernel of=xv6.img seek=1 conv=notrunc
# 需要启动块引导程序bootblock和系统内核kernel
# dd指令:把指定的输入文件拷贝到指定的输出文件中,并且在拷贝的过程中可以进行格式转换。
# conv=notrunc防止文件被截断(用于虚拟软盘)
# seek=1跳过一块再开始写



#6.生成启动引导块bootblock
bootblock: bootasm.S bootmain.c
    $(CC) $(CFLAGS) -fno-pic -O -nostdinc -I. -c bootmain.c
    $(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c bootasm.S
    $(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o bootmain.o
    $(OBJDUMP) -S bootblock.o > bootblock.asm
    $(OBJCOPY) -S -O binary -j .text bootblock.o bootblock
    ./sign.pl bootblock
# 主引导记录存入0x7C00地址
# 链接bootasm.o bootmain.o生成bootblock.o文件,objdump反编译输出到bootblock.asm,使用工具objcopy把bootblock.o的.text段(该段包含程序的可执行指令)拷贝出来生成bootblock
# 执行sign.pl,为bootblock设置大小512,得到BIOS之后执行的BootLoader



# #7.生成entryother 用于多核启动
# entryother: entryother.S
#   $(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c entryother.S
#   $(LD) $(LDFLAGS) -N -e start -Ttext 0x7000 -o bootblockother.o entryother.o
#   $(OBJCOPY) -S -O binary -j .text bootblockother.o entryother
#   $(OBJDUMP) -S bootblockother.o > entryother.asm



# #8.生成initcode 用于启动系统第一个进程
# initcode: initcode.S
#   $(CC) $(CFLAGS) -nostdinc -I. -c initcode.S
#   $(LD) $(LDFLAGS) -N -e start -Ttext 0 -o initcode.out initcode.o
#   $(OBJCOPY) -S -O binary initcode.out initcode
#   $(OBJDUMP) -S initcode.o > initcode.asm


# kernel: $(OBJS) entry.o entryother initcode kernel.ld
#   $(LD) $(LDFLAGS) -T kernel.ld -o kernel entry.o $(OBJS) -b binary initcode entryother
#   $(OBJDUMP) -S kernel > kernel.asm
#   $(OBJDUMP) -t kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > kernel.sym

clean: 
    rm -f *.tex *.dvi *.idx *.aux *.log *.ind *.ilg 
    *.o *.d *.asm *.sym vectors.S bootblock  
    kernel xv6.img  .gdbinit 

# run in emulators

bochs : fs.img xv6.img
    if [ ! -e .bochsrc ]; then ln -s dot-bochsrc .bochsrc; fi
    bochs -q

# try to generate a unique GDB port
GDBPORT = $(shell expr `id -u` % 5000 + 25000)
# QEMU's gdb stub command line changed in 0.11
QEMUGDB = $(shell if $(QEMU) -help | grep -q '^-gdb'; 
    then echo "-gdb tcp::$(GDBPORT)"; 
    else echo "-s -p $(GDBPORT)"; fi)
ifndef CPUS
CPUS := 2
endif
# 配置硬件信息
# -drive定义驱动器 file=指定镜像文件,磁盘类型raw -smp指定cpu -m指定内存
QEMUOPTS = -drive file=fs.img,index=1,media=disk,format=raw -drive file=xv6.img,index=0,media=disk,format=raw -smp $(CPUS) -m 512 $(QEMUEXTRA)


# qemu加载fs xv6镜像,-serial指定串口号mon:stdio,以及配置驱动器
qemu: fs.img xv6.img
    $(QEMU) -serial mon:stdio $(QEMUOPTS)

# qemu-memfs加载xv6memfs镜像,配置驱动器
qemu-memfs: xv6memfs.img
    $(QEMU) -drive file=xv6memfs.img,index=0,media=disk,format=raw -smp $(CPUS) -m 256

# -nographic非图形界面
qemu-nox: fs.img xv6.img
    $(QEMU) -nographic $(QEMUOPTS)

# 初始化调试器
.gdbinit: .gdbinit.tmpl
    sed "s/localhost:1234/localhost:$(GDBPORT)/" < $^ > $@

qemu-gdb: fs.img xv6.img .gdbinit
    @echo "*** Now run 'gdb'." 1>&2
    $(QEMU) -serial mon:stdio $(QEMUOPTS) -S $(QEMUGDB)

qemu-nox-gdb: fs.img xv6.img .gdbinit
    @echo "*** Now run 'gdb'." 1>&2
    $(QEMU) -nographic $(QEMUOPTS) -S $(QEMUGDB)

根据makefile中img的生成条件,不难看出 bootblock 应该是系统一开始引导阶段的逻辑,kernel 当然就是内核。第二段代码可以看出bootblock的生成依赖于bootasm.S和bootmain.c

xv6.img: bootblock kernel
    dd if=/dev/zero of=xv6.img count=10000
    dd if=bootblock of=xv6.img conv=notrunc
    dd if=kernel of=xv6.img seek=1 conv=notrunc

bootblock: bootasm.S bootmain.c
    $(CC) $(CFLAGS) -fno-pic -O -nostdinc -I. -c bootmain.c
    $(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c bootasm.S
    $(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o

首先来看bootasm.S

注意之前提到的Makefile中的一句

$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o

说明 bootblock 的代码段加载到内存 0x7C00 处,代码从 start 处开始执行。因此体现在代码中

;at physical address 0x7c00 and starts executing in real mode
;with %cs = 0 % ip = 7c00.

.code16            ;16 bit mode,此时寻址能力只有1MB,实模式下
.global start16 
start16:
    cli            ;关闭所有中断,确保引导代码正确执行

    ;寄存器初始化全部设置为0
    xorw %ax,%ax
    movw %ax,%ds
    movw %ax,%es
    movw %ax,%ss

然后打开A20 突破 1MB 内存寻址的限制。

;open A20
    movw msg_a20,%ax
    movw $0xe,%cx
    inb $0x92, %al
    orb $0x02, %al
    outb %al,$0x92

接下来为进入保护模式做准备,先准备GDT。

gdt:
  SEG_NULLASM                             # null seg
  SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)   # code seg
  SEG_ASM(STA_W, 0x0, 0xffffffff)         # data seg

代码中的宏展开形式为

gdt:
  .word 0, 0;
  .byte 0, 0, 0, 0                             # 空
  .word 0xffff, 0x0000;
  .byte 0x00, 0x9a, 0xcf, 0x00                 # 代码段
  .word 0xffff, 0x0000;
  .byte 0x00, 0x92, 0xcf, 0x00                 # 数据段

这里的不同在于S E这两位,

S:       1 代表数据段、代码段或堆栈段,0 代表系统段如中断门或调用门

E:       1 代表代码段,可执行标记,0 代表数据段

代码段:

7ae538b93206fb17b394532895858db7.png

数据段:

3263ce0b5a7cbf67c04d22814489ed3c.png

然后进入保护模式,使用专门的gdtr lgdt来把gdt的位置通知cpu。

lgdt   gdtdesc

gdtdesc如下

gdtdesc:
  .word   (gdtdesc - gdt - 1)             # 16 位的 gdt 大小sizeof(gdt) - 1
  .long   gdt                             # 32 位的 gdt 所在物理地址

然后打开cr0的PE位最终进入保护模式

f6170fbc2392264ef3e80e0390458c43.png
movl    %cr0, %eax
orl     $CR0_PE, %eax
movl    %eax, %cr0

进入保护模式

使用长跳转指令进入

ljmp    $(SEG_KCODE<<3), $start32
#define SEG_KCODE 1  // kernel code

分段式保护模式下“段基址”(基地址)不再是内存地址,而是 GDT 表的下标。GDT 表最大可以有 8192 个表项(段描述符),2^13 = 8192,所以保存着“段基址”的 16 位段寄存器只需要其中的 13 位就可以表示一个 GDT 表的下标,其余的 3 位可用作他用。

f028aef74f00d906a4623d844c100f20.png

这里高 13 位刚好是 1,而我们的 GDT 里下标位 1 的内存段正好是我们的“代码段”,而“代码段”我们在 GDT 的“段描述符”中设置了它的其实内存地址是 0x00000000 ,内存段长度是 0xfffff,这是完整的 4GB 内存。所以这里的跳转语句选择了“代码段,后面的偏移量$start32直接对应了代码位置。通过这个跳转实际上 CPU 就会跳转到 bootasm.S 文件的 start32 标识符处继续执行。

接着

start32:
  # Set up the protected-mode data segment registers
  # 像上面讲 ljmp 时所说的,这时候已经在保护模式下了
  # 数据段在 GDT 中的下标是 2,所以这里数据段的段选择子是 2 << 3 = 0000 0000 0001 0000
  # 这 16 位的段选择子中的前 13 位是 GDT 段表下标,这里前 13 位的值是 2 代表选择了数据段
  # 这里将 3 个数据段寄存器都赋值成数据段段选择子的值
  movw    $(SEG_KDATA<<3), %ax    # Our data segment selector  段选择子赋值给 ax 寄存器
  movw    %ax, %ds                # -> DS: Data Segment        初始化数据段寄存器
  movw    %ax, %es                # -> ES: Extra Segment       初始化扩展段寄存器
  movw    %ax, %ss                # -> SS: Stack Segment       初始化堆栈段寄存器
  movw    $0, %ax                 # Zero segments not ready for use  ax 寄存器清零
  movw    %ax, %fs                # -> FS                      辅助寄存器清零
  movw    %ax, %gs                # -> GS                      辅助寄存器清零

  # Set up the stack pointer and call into C.
  movl    $start, %esp
  call    bootmain                #最终跳转到bootmain.c

bootmain的实现依赖于elf,ELF 文件格式是 Linux 下可执行文件的标准格式。先看elf格式定义:

#define ELF_MAGIC 0x464C457FU  // "x7FELF" in little endian

// ELF 文件格式的头部
struct elfhdr {
  uint magic;       // 4 字节,为 0x464C457FU(大端模式)或 0x7felf(小端模式)
                      // 表明该文件是个 ELF 格式文件

  uchar elf[12];    // 12 字节,每字节对应意义如下:
                    //     0 : 1 = 32 位程序;2 = 64 位程序
                    //     1 : 数据编码方式,0 = 无效;1 = 小端模式;2 = 大端模式
                    //     2 : 只是版本,固定为 0x1
                    //     3 : 目标操作系统架构
                    //     4 : 目标操作系统版本
                    //     5 ~ 11 : 固定为 0

  ushort type;      // 2 字节,表明该文件类型,意义如下:
                    //     0x0 : 未知目标文件格式
                    //     0x1 : 可重定位文件
                    //     0x2 : 可执行文件
                    //     0x3 : 共享目标文件
                    //     0x4 : 转储文件
                    //     0xff00 : 特定处理器文件
                    //     0xffff : 特定处理器文件

  ushort machine;   // 2 字节,表明运行该程序需要的计算机体系架构,
                    // 这里我们只需要知道 0x0 为未指定;0x3 为 x86 架构

  uint version;     // 4 字节,表示该文件的版本号

  uint entry;       // 4 字节,该文件的入口地址,没有入口(非可执行文件)则为 0

  uint phoff;       // 4 字节,表示该文件的“程序头部表”相对于文件的位置,单位是字节

  uint shoff;       // 4 字节,表示该文件的“节区头部表”相对于文件的位置,单位是字节

  uint flags;       // 4 字节,特定处理器标志

  ushort ehsize;    // 2 字节,ELF文件头部的大小,单位是字节

  ushort phentsize; // 2 字节,表示程序头部表中一个入口的大小,单位是字节

  ushort phnum;     // 2 字节,表示程序头部表的入口个数,
                    // phnum * phentsize = 程序头部表大小(单位是字节)

  ushort shentsize; // 2 字节,节区头部表入口大小,单位是字节

  ushort shnum;     // 2 字节,节区头部表入口个数,
                    // shnum * shentsize = 节区头部表大小(单位是字节)

  ushort shstrndx;  // 2 字节,表示字符表相关入口的节区头部表索引
};

// 程序头表
struct proghdr {
  uint type;        // 4 字节, 段类型
                    //         1 PT_LOAD : 可载入的段
                    //         2 PT_DYNAMIC : 动态链接信息
                    //         3 PT_INTERP : 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小
                    //         4 PT_NOTE : 指定辅助信息的位置和大小
                    //         5 PT_SHLIB : 保留类型,但具有未指定的语义
                    //         6 PT_PHDR : 指定程序头表在文件及程序内存映像中的位置和大小
                    //         7 PT_TLS : 指定线程局部存储模板
  uint off;         // 4 字节, 段的第一个字节在文件中的偏移
  uint vaddr;       // 4 字节, 段的第一个字节在内存中的虚拟地址
  uint paddr;       // 4 字节, 段的第一个字节在内存中的物理地址(适用于物理内存定位型的系统)
  uint filesz;      // 4 字节, 段在文件中的长度
  uint memsz;       // 4 字节, 段在内存中的长度
  uint flags;       // 4 字节, 段标志
                    //         1 : 可执行
                    //         2 : 可写入
                    //         4 : 可读取
  uint align;       // 4 字节, 段在文件及内存中如何对齐
};

bootmain.c的代码是引导工作的最后部分,它负责将内核从硬盘上加载到内存中,然后开始执行内核中的程序。

#define SECTSIZE  512  // 硬盘扇区大小 512 字节

void
bootmain(void)
{
  struct elfhdr *elf;
  struct proghdr *ph, *eph;
  void (*entry)(void);
  uchar* pa;

  // 从 0xa0000 到 0xfffff 的物理地址范围属于设备空间,
  // 所以内核放置在 0x10000 处开始
  elf = (struct elfhdr*)0x10000;  // scratch space

  // 从内核所在硬盘位置读取一内存页 4kb 数据
  readseg((uchar*)elf, 4096, 0);

  // 判断是否为 ELF 文件格式
  if(elf->magic != ELF_MAGIC)
    return;  // let bootasm.S handle error

  // 加载 ELF 文件中的程序段 (ignores ph flags).
  ph = (struct proghdr*)((uchar*)elf + elf->phoff);
  eph = ph + elf->phnum;
  for(; ph < eph; ph++){
    pa = (uchar*)ph->paddr;
    readseg(pa, ph->filesz, ph->off);
    if(ph->memsz > ph->filesz)
      stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);
  }

  // Call the entry point from the ELF header.
  // Does not return!
  entry = (void(*)(void))(elf->entry);
  entry();
}


void
readseg(uchar* pa, uint count, uint offset)  // 0x10000, 4096(0x1000), 0
{
  uchar* epa;

  epa = pa + count;  // 0x11000

  // 根据扇区大小 512 字节做对齐
  pa -= offset % SECTSIZE;

  // bootblock 引导区在第一扇区(下标为 0),内核在第二个扇区(下标为 1)
  // 这里做 +1 操作是统一略过引导区
  offset = (offset / SECTSIZE) + 1;

  // If this is too slow, we could read lots of sectors at a time.
  // We'd write more to memory than asked, but it doesn't matter --
  // we load in increasing order.
  // 一次读取一个扇区 512 字节的数据
  for(; pa < epa; pa += SECTSIZE, offset++)
    readsect(pa, offset);
}

看一下 xv6 的 Makefile 文件关于 xv6.img 构建过程

xv6.img: bootblock kernel
    dd if=/dev/zero of=xv6.img count=10000
    dd if=bootblock of=xv6.img conv=notrunc
    dd if=kernel of=xv6.img seek=1 conv=notrunc

可以看出 xv6.img 是一个由 10000 个扇区组成的(512b x 10000 = 5 MB),而里面包含的只有 bootblockkernel 两个块,通过名字我们不难看出 bootblock 就是引导区,它的大小正好是 512 字节即一个磁盘扇区大小。

最后我们看一下从磁盘读取内核到内存的方法实现,看看是怎样通过向特定端口发送数据来达到操作磁盘目的的。具体的说明请看代码附带的注释。

// Read a single sector at offset into dst.
// 这里使用的是 LBA 磁盘寻址模式
// LBA是非常单纯的一种寻址模式﹔从0开始编号来定位区块,
// 第一区块LBA=0,第二区块LBA=1,依此类推
void
readsect(void *dst, uint offset)      // 0x10000, 1
{
  // Issue command.
  waitdisk();
  outb(0x1F2, 1);                     // 要读取的扇区数量 count = 1
  outb(0x1F3, offset);                // 扇区 LBA 地址的 0-7 位
  outb(0x1F4, offset >> 8);           // 扇区 LBA 地址的 8-15 位
  outb(0x1F5, offset >> 16);          // 扇区 LBA 地址的 16-23 位
  outb(0x1F6, (offset >> 24) | 0xE0); // offset | 11100000 保证高三位恒为 1
                                      //         第7位     恒为1
                                      //         第6位     LBA模式的开关,置1为LBA模式
                                      //         第5位     恒为1
                                      //         第4位     为0代表主硬盘、为1代表从硬盘
                                      //         第3~0位   扇区 LBA 地址的 24-27 位
  outb(0x1F7, 0x20);                  // 20h为读,30h为写

  // Read data.
  waitdisk();
  insl(0x1F0, dst, SECTSIZE/4);
}

内存布局示意图

0x00000000
+------------------------------------------------------------------------—+
|        0x7c00      0x7d00         0x10000                               |
|    栈    |  引导程序  |                |    内核                          |
+-------------------------------------------------------------------------+
                                                                 0xffffffff

栈顶0x7c00处并且向下增长。

# Set up the stack pointer and call into C.
  movl    $start, %esp         # 栈顶被放置在 0x7C00 处,即$start

准备添加的额外实现

1.添加e820内存探测

2.添加AT&T打印函数

3.尝试简单快速版本的A20开启方式

4.使用bochs模拟并打印出asm文件

参考

https://www.cnblogs.com/wuhualong/p/ucore_lab1_exercise1_report.html​www.cnblogs.com 知乎 - 安全中心​iluvrachel.github.io http://leenjewel.github.io/blog/2015/05/26/%5B%28xue-xi-xv6%29%5D-jia-zai-bing-yun-xing-nei-he/​leenjewel.github.io

以及xv6初版commit

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值