xv6/risc-v

十月 13, 2022 [OS] #OS #xv6 #jos

xv6/risc-v

some elf tool

elfutils

/usr/bin/eu-addr2line /usr/bin/eu-ar – alternative to ar, to create, manipulate archive files /usr/bin/eu-elfcmp /usr/bin/eu-elflint – compliance check against gABI and psABI specifications /usr/bin/eu-findtextrel – find text relocations /usr/bin/eu-ld – combining object and archive files /usr/bin/eu-make-debug-archive /usr/bin/eu-nm – display symbols from object/executable files /usr/bin/eu-objdump – show information of object files /usr/bin/eu-ranlib – create index for archives for performance /usr/bin/eu-readelf – human-readable display of ELF files /usr/bin/eu-size – display size of each section (text, data, bss, etc) /usr/bin/eu-stack – show the stack of a running process, or coredump /usr/bin/eu-strings – display textual strings (similar to strings utility) /usr/bin/eu-strip – strip ELF file from symbol tables /usr/bin/eu-unstrip – add symbols and debug information to stripped binary 注:elfutils 包是个很好的开始,它包含分析用的大多数工具

elfkickers

/usr/bin/ebfc – compiler for Brainfuck programming language /usr/bin/elfls – shows program headers and section headers with flags /usr/bin/elftoc – converts a binary into a C program /usr/bin/infect – tool to inject a dropper, which creates setuid file in /tmp /usr/bin/objres – creates an object from ordinary or binary data /usr/bin/rebind – changes bindings/visibility of symbols in ELF file /usr/bin/sstrip – strips unneeded components from ELF file 注: ELFKickers 包专注操作 ELF 文件,分析畸形 ELF 文件会很有用。

pax-utils

/usr/bin/dumpelf – dump internal ELF structure /usr/bin/lddtree – like ldd, with levels to show dependencies /usr/bin/pspax – list ELF/PaX information about running processes /usr/bin/scanelf – wide range of information, including PaX details /usr/bin/scanmacho – shows details for Mach-O binaries (Mac OS X) /usr/bin/symtree – displays a leveled output for symbols 注: 包里面的工具可以搜索整个文件。大型分析有用,目标集中在搜集 PaX 信息。除了 ELF, Mach-O 文件也支持。 输出示例:

  scanelf -a /bin/ps
   TYPE    PAX   PERM ENDIAN STK/REL/PTL TEXTREL RPATH BIND FILE
  ET_EXEC PeMRxS 0755 LE RW- R-- RW-    -      -   LAZY /bin/ps

/usr/bin/execstack – display or change if stack is executable /usr/bin/prelink – remaps/relocates calls in ELF files, to speed up the process

readelf info

PhysAddr only user physical depend program? offset --> 相对elf header头部最开开始的物理地址偏移 VirtAddr --> 加载到内存时候的虚拟地址 PhysAddr --> 无意义 FileSiz --> elf中program尺寸 MemSiz --> program 加载到meme尺寸,含bss

readelf -l -W /usr/bin/ls

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x6ab0
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R   0x8
  INTERP         0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x003428 0x003428 R   0x1000
  ...

lab1

BIOS 0xffff0

Switch from real(16bit) to protected mode(32bit), using a bootstrap GDT

lgdt gdtdesc movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0

Jump to next instruction, but in 32-bit code segment.

Switches processor into 32-bit mode.

ljmp $PROT_MODE_CSEG,$protcseg ...

boot 0x7c00 ... readseg(...) ... call bootmain ((void (*)(void)) (ELFHDR->e_entry))();

kernel .globl _start _start = RELOC(entry)

.globl entry entry: movw $0x1234,0x472 # warm boot ...

kernel.ld

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386") OUTPUT_ARCH(i386) ENTRY(_start)

SECTIONS { /* Link the kernel at this address: "." means the current address */ . = 0xF0100000;

/* AT(...) gives the load address of this section, which tells the boot loader where to load the kernel in physical memory / .text : AT(0x100000) { (.text .stub .text. * .gnu.linkonce.t.* ) }

git clone https://pdos.csail.mit.edu/6.828/2018/jos.git
cd jos && make qemu-nox-gdb & && make gdb
gdb> symbol-file obj/boot/boot.out # boot.out which generate boot is elf file
gdb> add symbol-file obj/kern/kernel
gdb> b *0xffff0
gdb> b *0x7c00
gdb> b bootmain

stack layout

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

reference

#include "asm.h"
#include "memlayout.h"
#include "mmu.h"

# Start the first CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.code16                       # Assemble for 16-bit mode
.globl start
start:
  cli                         # BIOS enabled interrupts; disable

  # Zero data segment registers DS, ES, and SS.
  xorw    %ax,%ax             # Set %ax to zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment

  # Physical address line A20 is tied to zero so that the first PCs 
  # with 2 MB would run software that assumed 1 MB.  Undo that.
  # ///////////////////////////////////////////////////////////////
  # 这里用了通过键盘控制器端口的方法打开 A20 地址
  # 打开 A20 地址线的方法有三种,具体可参考:
  #     http://kernelx.weebly.com/a20-address-line.html
  #
  # 下面都是针对 804x 键盘控制器端口的操作,这里只介绍用到的两个端口
  #     0x64  从该端口执行 in 读取操作时,读取状态寄存器,8 位,第二
  #           位标识输入缓冲是否有数据所以下面用 0x2 来 test 输入缓冲是否有数据
  #           向该端口执行 out 写操作时,代表向键盘控制器发送命令,发送 0xd1 代
  #           表要向键盘控制器的 P2 端口写入数据,要写入的数据通过 0x60 端口传入
  #
  #     P2    端口,8位,第二位就是 A20 开关,所以在 seta20.2 代码段将 0xdf 通过
  #           0x60 端口写入到 P2 时就会把 A20 置位,A20 打开后,进入保护模式
seta20.1:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.1                # 通过 0x64 状态寄存器的第二位判断键盘缓冲区里是否有数据,如果还有数据,则等待

  movb    $0xd1,%al               # 0xd1 -> port 0x64
  outb    %al,$0x64               # 0xd1 代表向 804x 控制器的 P2 写数据

seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2                # 同上,继续判断键盘缓冲区是否有数据,如果有数据,则等待

  movb    $0xdf,%al               # 0xdf -> port 0x60
  outb    %al,$0x60               # 将 0xdf 11011111 写入键盘控制器的 P2 口,意味着 A20 打开,进入保护模式

  # Switch from real to protected mode.  Use a bootstrap GDT that makes
  # virtual addresses map directly to physical addresses so that the
  # effective memory map does not change during the transition.
  # 控制寄存器 CR0 为 32 位寄存器,和保护模式相关的控制位
  # **注意大小端**
  # 
  # |31|--------30~1--------| 0|
  # ----------------------------
  # |PG|--------------------|PE|
  # PG : 开启分页式
  # PE : 开启保护模式
  # CR0_PE 在 mmu.h 中定义的 32 位用于控制 CR0
  # CR0_PE : 0x00000001
  # | 0|-------都是0--------| 1|
  # CR0_PE 与 cr0 做或运算保证 PG = 0, PE = 1
  # 至此开启了保护模式,基于分段式,没有打开分页式
  lgdt    gdtdesc          # 先把 GDT 告诉 CPU
  movl    %cr0, %eax       # 把 CR0 寄存器的值复制给 eax 寄存器
  orl     $CR0_PE, %eax    # 与 CR0_PE 做或运算,打开保护模式
  movl    %eax, %cr0

//PAGEBREAK!
  # Complete transition to 32-bit protected mode by using long jmp
  # to reload %cs and %eip.  The segment descriptors are set up with no
  # translation, so that the mapping is still the identity mapping.
  # 
  # 到这里已经进入保护模式了,这里做代码跳转的时候就变成了基于分段式的跳转
  # 分段式跳转 ljmp  段选择子,  段内偏移量
  # 在 GDT 中我们的代码段下标是 1 ,所以这里段选择子是 1 << 3 = 0000 0000 0000 1000
  # 在 16 位的段选择子中前 13 位是 GDT 段表下标,这里前 13 位值是 1 代表选择代码段
  # 而我们的代码段是从 0 开始到 4GB 结尾的,所以这里偏移量不用做转换,还是原偏移即可
  ljmp    $(SEG_KCODE<<3), $start32

.code32  # Tell assembler to generate 32-bit code now.
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            # 栈顶被设置为 0x7C00 处
  call    bootmain

  # If bootmain returns (it should not), trigger a Bochs
  # breakpoint if running under Bochs, then loop.
  movw    $0x8a00, %ax            # 0x8a00 -> port 0x8a00
  movw    %ax, %dx
  outw    %ax, %dx
  movw    $0x8ae0, %ax            # 0x8ae0 -> port 0x8a00
  outw    %ax, %dx
spin:
  jmp     spin

# **注意大小端**
# Bootstrap GDT
# 每个 GDT 项 8 字节
#
# |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9|8|7|6|5|4|3|2|1|0|
# |--------------------------------------------------------------------------------------|
# |        基地址         | G|DB|XX|AA|   Limit   | P| DPL | S| E|ED|RW|A|   基地址      |
# |--------------------------------------------------------------------------------------|
# |               基地址                          |          Limit                       |
# |--------------------------------------------------------------------------------------|
#
# 标记位解释:
# P:    0 本段不在内存中
# DPL:  访问该段内存所需权限等级 00 -- 11,0为最大权限级别
# S:    1 代表数据段、代码段或堆栈段,0 代表系统段如中断门或调用门
# E:     1 代表代码段,可执行标记,0 代表数据段
# ED:    0 代表忽略特权级,1 代表遵守特权级
# RW:    如果是数据段(E=0)则1 代表可写入,0 代表只读
#        如果是代码段(E=1)则1 代表可读取,0 代表不可读取
# A:     1 表示该段内存访问过,0 表示没有被访问过
# G:     1 表示 20 位段界限单位是 4KB,最大长度 4GB,
#        0 表示 20 位段界限单位是 1 字节,最大长度 1MB
# DB:    1 表示地址和操作数是 32 位,0 表示地址和操作数是 16 位
# XX:    保留位永远是 0
# AA:    给系统提供的保留位
#
# |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9|8|7|6|5|4|3|2|1|0|
# |--------------------------------------------------------------------------------------|
# |        基地址         | G|DB|XX|AA|   Limit   | P| DPL | S| E|ED|RW|A|   基地址      |
# |--------------------------------------------------------------------------------------|
# |               基地址                          |          Limit                       |
# |--------------------------------------------------------------------------------------|
#
# 一共 3 个 GDT 项
# 第一个 GDT 项目置空不用
# 第二个 GDT 为代码段,可执行,只读(写好的程序指令是不允许更改的)
# 第三个 GDT 为数据段,可读,可写,不可执行
# 采用平坦内存布局,每个分段均从地址 0x0 开始到 0xffffffff 结束,涵盖整个 4GB 内存寻址空间
# SEG_ASM 宏在 asm.h 中,下面翻译一下最后两个 GDT 项目的数据值
# 代码段 GDT 的布局:
#   0xffff,0x0000,0x00,0x9a,0xcf,0x00
#
# |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
# |-------------------------------------------------------------------------------------|
# |         0x00          | 1| 1| 0| 0|     f     | 1|0   0| 1| 1  0 1 0|     0x00      |
# |-------------------------------------------------------------------------------------|
# |               0x0000                          |          0xffff                     |
# |-------------------------------------------------------------------------------------|
#
# 数据段 GDT 的布局:
#   0xffff,0x0000,0x00,0x92,0xcf,0x00
#
# |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
# |-------------------------------------------------------------------------------------|
# |         0x00          | 1| 1| 0| 0|     f     | 1|0   0| 1| 0  0 1 0|     0x00      |
# |-------------------------------------------------------------------------------------|
# |               0x0000                          |          0xffff                     |
# |-------------------------------------------------------------------------------------|
#
.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULLASM                             # null seg
  SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)   # code seg
  SEG_ASM(STA_W, 0x0, 0xffffffff)         # data seg

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

// Boot loader.
// 
// Part of the boot sector, along with bootasm.S, which calls bootmain().
// bootasm.S has put the processor into protected 32-bit mode.
// bootmain() loads an ELF kernel image from the disk starting at
// sector 1 and then jumps to the kernel entry routine.

#include "types.h"
#include "elf.h"
#include "x86.h"
#include "memlayout.h"

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

void readseg(uchar*, uint, uint);

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

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

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

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

  // 加载 ELF 文件中的程序段 (ignores ph flags).
  // 找到内核 ELF 文件的程序头表
  ph = (struct proghdr*)((uchar*)elf + elf->phoff);
  // 内核 ELF 文件程序头表的结束位置
  eph = ph + elf->phnum;
  // 开始将内核 ELF 文件程序头表载入内存
  for(; ph < eph; ph++){
    pa = (uchar*)ph->paddr;
    readseg(pa, ph->filesz, ph->off);
    // 如果内存大小大于文件大小,用 0 补齐内存空位
    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!
  // 从内核 ELF 文件入口点开始执行内核
  entry = (void(*)(void))(elf->entry);
  entry();
}

void
waitdisk(void)
{
  // Wait for disk ready.
  // xxxxxxxx & 11000000 != 01000000
  // 0x1F7 状态位
  //     bit 7 = 1  控制器忙
  //     bit 6 = 1  驱动器就绪
  //     bit 5 = 1  设备错误
  //     bit 4        N/A
  //     bit 3 = 1  扇区缓冲区错误
  //     bit 2 = 1  磁盘已被读校验
  //     bit 1        N/A
  //     bit 0 = 1  上一次命令执行失败
  while((inb(0x1F7) & 0xC0) != 0x40)
    ;
}

// 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);
}

// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked.
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);
}

footprint

https://pdos.csail.mit.edu/6.828/2018/labguide.html#debug-kernel http://www.luxu.info/2017/11/22/xv6%E4%BB%A3%E7%A0%81%E5%AD%A6%E4%B9%A03/ http://leenjewel.github.io/blog/2014/07/29/%5B%28xue-xi-xv6%29%5D-cong-shi-mo-shi-dao-bao-hu-mo-shi/ https://github.com/leenjewel/xv6_learn/blob/learn/bootasm.S https://github.com/leenjewel/xv6_learn/blob/learn/elf.h