Chapter 05無料公開

Section Header TableとString Table

Drumato
Drumato
2021.08.29に更新

Section Header TableとString Table

elf header parserが完成すると,
section header tableのparserを作れるようになります.

ここで,以下2つの前提知識について復習しておきます.

  • section nameはsection header tableに直接埋め込まれない
    • これはsection headerを固定長で実現するために必要
    • ということでelfのstring table formatの理解が必要
  • sectionの内容はsection typeによって異なる
    • 例えばsymtabならsymbol entryの配列になっている
    • progbitsならベタに機械語が並んでいる

section header

まずはsection headerの中身です.
ELF Headerと同じように一部のみ取り上げます.

name size(bytes) description
name idx 4 section nameのindex
type 4 PROGBITS/NOTEなどセクションの種類
flags 8 mergeを許したりwriteを許したり
addr 8 sectionをどのvirtual addressにmappingするか
offset 8 section contentsの開始offset
size 8 section contentsのfile上でのsize
link 4 section typeによって異なる使われ方をする
info 4 section typeによって異なる使われ方をする
entry_size 8 section contentsがtable formatの場合に使われる

このように,section nameはindirectに読み込むようになっています.
そして,そのsection nameが書き込まれているsectionが存在し(ややこしい),
そのsection indexはELF headerに書き込まれています.

つまり,以下の手順で "section nameが格納されているsectionのcontents" を取得する必要があります.

  • ELF headerを読む
  • section header tableを読む
  • header.shstrndx からsection name tableを取ってくる
  • それをparseして使える形にする
  • shdr.sh_name を渡してsection nameを取り出す

面白くなってきました,やっていきましょう

string table format

とその前に,肝心のstring table formatについて説明しておきますが,
大体こんな感覚を持ってくれれば良いと思います.

section_names = section_name \x00
section_name = take_while(printable_ascii)

\x00?と表すと,
?.text?.symtab?.strtab のようになるわけです.

null section (header)

あれ?最初間違ってない?と思った方は 鋭い
実はelfには "null section(header)" を含めるという暗黙の了解があります.
もしかしたら仕様に書いてあるのかもしれませんが,
私はelfの勉強はreadelf8割でやったので...

つまり,以下を仮定するのです.

  • section header tableの先頭にはnull section headerがある
    • このheaderはちゃんと sizeof(Elf64_Shdr) 分のサイズを持つので注意
    • shdr.sh_size = 0 なので,size0のsectionがmappingされていることになります
  • もちろんnameもないので, <null-section-name>?.text?.symtab? となるわけです.
    • 実際には \x00 (section_name \x00)+ のようなformatだと考えることもできます
    • しかし経験上,前者のイメージでparseしたほうがキレイになる

section header parser

それではまずはsection header parserを作ります.
基本的なやり方はch03と同じです.
testcodeは省略します.

// src/parser/section.rs
fn parse_64bit_section_header(raw: &[u8]) -> IResult<&[u8], SectionHeader64> {
    let (r, name_idx) = le_u32(raw)?;
    let (r, ty) = parse_section_type(r)?;
    let (r, flags) = le_u64(r)?;
    let (r, addr) = le_u64(r)?;
    let (r, offset) = le_u64(r)?;
    let (r, size) = le_u64(r)?;
    let (r, link) = le_u32(r)?;
    let (r, info) = le_u32(r)?;
    let (r, addr_align) = le_u64(r)?;
    let (r, entry_size) = le_u64(r)?;

    Ok((
        r,
        SectionHeader64 {
            name_idx,
            ty,
            flags,
            addr,
            offset,
            size,
            link,
            info,
            addr_align,
            entry_size,
        },
    ))
}

あとはこれをsht_entries分回せば良いだけですね.
nomでは sub parserのfn signatureがRustのtype systemでガッチガチに決まっている ので,
closureを作るようにしてうまくhackします.
こういうのがstrongly typed languageの楽しいところですね.

// src/parser/section.rs
pub fn parse_64bit_section_header_table<'a>(
    entries: usize,
) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], Vec<SectionHeader64>> {
    move |raw: &'a [u8]| count(parse_64bit_section_header, entries)(raw)
}

string table entry parser

ここではstring tableのentryを一つparseする関数を作ります.

// src/parser/string_table.rs
pub fn parse_string_table_entry(raw: &[u8]) -> IResult<&[u8], String> {
    terminated(parse_ascii_string, tag(b"\x00"))(raw)
}

fn parse_ascii_string(raw: &[u8]) -> IResult<&[u8], String> {
    map(
        take_while(|c: u8| c.is_ascii_graphic()),
        |raw_name: &[u8]| String::from_utf8(raw_name.to_vec()).unwrap().to_string(),
    )(raw)
}

elf file parser

まだprogram header tableを作っていませんが,
せっかくなのでelf全体をparseする関数を先に作ってしまいましょう.
ここのlogicを理解するには更に一つ前提知識が必要です.

// src/parser/file.rs
pub fn parse_64bit_elf(raw: &[u8]) -> IResult<&[u8], Elf64> {
    let (_, header) = parse_64bit_elf_header(raw)?;
    let (_, sht) = parse_64bit_section_header_table(header.sht_entries as usize)(
        &raw[header.sht_offset as usize..],
    )?;
    let raw_section_names_offset = sht[header.sht_string_table_index as usize].offset;
    let raw_section_names = &raw[raw_section_names_offset as usize..];
    let sections = sht
        .into_iter()
        .map(|header| Section64 {
            name: parse_string_table_entry(&raw_section_names[header.name_idx as usize..])
                .unwrap()
                .1,
            header,
        })
        .collect::<Vec<Section64>>();

    Ok((&raw[raw.len()..], Elf64 { header, sections }))
}

それは,
section_names[idx]sht[idx] は別に対応しない ということです.

例えば sht[1] が.text sectionだったとして,
section_name_table[1].strtab である可能性があります.
これはlinkerやasmがsize reductionを適用したりする過程でこのようになります.
ということで, sht.into_iter().zip(raw_section_names.into_iter()) を使う事はできず,
頑張って毎回取ってこないといけないんですね.

modify main fn

ということでmain関数を書き換えましょう.
以下を追加します.

    {
        for (i, sct) in elf_file.sections.iter().enumerate() {
            println!("Sections[{}]:", i);
            println!("\tName: {}", sct.name);
            println!("\tType: {:?}", sct.header.ty);
            println!("\tSize: 0x{:x}", sct.header.size);
            println!("\tEntry Size: 0x{:x}", sct.header.entry_size);
        }
    }

実行結果は次のようになります.

$ cargo run target/debug/elf-parser-in-rust-book
ELF Header:
        Class: Bit64
        Data: Lsb
        Entry: 0x9720
        PHT Info:
                Offset: 64
                Entries: 12
                Entry Size: 0x38
        SHT Info:
                Offset: 0x46fcb0
                Entries: 43
                Entry Size: 0x40
                Name Table Index: 42
Sections[0]:
        Name:
        Type: Null
        Size: 0x0
        Entry Size: 0x0
Sections[1]:
        Name: .interp
        Type: ProgBits
        Size: 0x1c
        Entry Size: 0x0
Sections[2]:
        Name: .note.ABI-tag
        Type: Note
        Size: 0x20
        Entry Size: 0x0
Sections[3]:
        Name: .note.gnu.build-id
        Type: Note
        Size: 0x24
        Entry Size: 0x0
Sections[4]:
        Name: .gnu.hash
        Type: Unknown
        Size: 0x30
        Entry Size: 0x0
Sections[5]:
        Name: .dynsym
        Type: DynSym
        Size: 0x708
        Entry Size: 0x18