Explore "Full-Stack" in depth!

情報系の専門学校で、今は機械学習に的を絞って学習中。プログラミングを趣味でやりつつ、IT系のあらゆる知識と技術を身に付けるべく奮闘中。

readelf -lを簡易実装してELFフォーマットの理解を深める。

目次

概要

こちらの記事の続きです

drumato.hatenablog.com

ELFヘッダのあとは、
プログラムヘッダの解析 に移るのが自然な流れだと思います。

今回も ソースコードベース で見ていくことで、
バイナリに慣れていない人にも理解できる記事を目指します。

GitHubを参照しながら読み進めて頂ければと思います。

github.com


前提:プログラムヘッダテーブルとは?

私の記事を読む前に、
こちらの記事を読んでおくことをおすすめします。

/usr/include/elf.hの内容を解説する記事は多いですが、
構造体の各メンバ・ヘッダ情報の意味についてここまで詳しく解説している日本語の記事はこれ以外ないかも。

こちらの記事に記載されている情報を参考にしながら、
簡単に前提知識をお話しておきます。


プログラムヘッダテーブルとは、
セグメント情報を格納した配列 です。

OSがプロセス生成して実行するようなプログラムには
必ずこのプログラムヘッダテーブルが存在しています。

具体的にどんな情報が入っているかを紹介します。

  • セグメント情報
    • どのようにこのセグメントを解釈すべきかを識別する
  • セグメントのファイル上でのオフセット
  • セグメントにマッピングされた仮想アドレス
  • 物理アドレッシングのシステム上で用いるためのフィールド
  • セグメントのファイルイメージ上のバイト数
  • セグメントのメモリイメージ上のバイト数
  • セグメントの属性を示すフラグ
  • セグメントのアラインメント

前回の記事で説明したとおり、
ELFヘッダには プログラムヘッダテーブル (を参照するため)の情報が格納されています。

実際にELFヘッダを解析して、
プログラムヘッダテーブルを参照するための情報を見てみましょう。

f:id:orangebladdy:20190423182511j:plain

今回見たいのは、

  • Start of program headers
  • Size of program headers
  • Number of program headers
  • ( size of this header )
    • もしELFヘッダが64バイトでなければ考慮する必要がある
    • 普段は特に関係ない

です。

これらはそれぞれ、

  • プログラムヘッダテーブルが始まる場所( オブジェクトファイル上で)
  • プログラムヘッダテーブルのサイズ( これは 1配列要素 を示す)
  • プログラムヘッダの数

を表します。

プログラムヘッダテーブルを解析するには、
これら情報を活用する必要があります。

逆に言えば、
ELFヘッダを見ればプログラムヘッダテーブルがわかる ということです。


本題

コードリーディングで情報集め

まずはLinuxのヘッダファイルを参照して、
コーディングの為の情報を集めます。

/usr/include/elf.hでは、
プログラムヘッダの構造体が定義されています。

typedef struct elf32_phdr{
  Elf32_Word    p_type;
  Elf32_Off p_offset;
  Elf32_Addr    p_vaddr;
  Elf32_Addr    p_paddr;
  Elf32_Word    p_filesz;
  Elf32_Word    p_memsz;
  Elf32_Word    p_flags;
  Elf32_Word    p_align;
} Elf32_Phdr;

typedef struct elf64_phdr {
  Elf64_Word p_type;
  Elf64_Word p_flags;
  Elf64_Off p_offset;       /* Segment file offset */
  Elf64_Addr p_vaddr;       /* Segment virtual address */
  Elf64_Addr p_paddr;       /* Segment physical address */
  Elf64_Xword p_filesz;     /* Segment size in file */
  Elf64_Xword p_memsz;      /* Segment size in memory */
  Elf64_Xword p_align;      /* Segment alignment, file & memory */
} Elf64_Phdr;

先程列挙した情報がメンバとして定義されているのがわかると思います。

次に、readelfを参照し、
どのように出力すればいいのかを確認します。

github.com

を見てみましょう。

今回も

drumato.hatenablog.com

の記事でやったように、

  • オプション引数を解析するparse_args()を見る
  • 立ったフラグ(今回は do_segments )によって処理・フローが変わる部分を見る

という流れでコードを読みます。
巨大なソースコードの場合、
(優れたサービス・ソフトウェアであれば) 参照しやすい・理解しやすいコード設計に尽力しているはずです。

今回はフラグという管理方法をとっていますね。

static void
parse_args (Filedata * filedata, int argc, char ** argv)
{
  //省略 
  case 'l':
      do_segments = TRUE;
      break;

  //省略2
}

見つけました。

do_segmentsgrepをかけます。

当該フラグによって出力される部分
を見つけ、
内容を読んでいきます。

とても長いので、
今回実装した部分のみを抜き出して紹介します。

if (do_segments)
{
  if (filedata->file_header.e_phnum > 1)
      printf (_("\nProgram Headers:\n"));
  else
      printf (_("\nProgram Headers:\n"));

  if (is_32bit_elf)
      printf(      _("  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align\n"));
  else if (do_wide)
      printf(_("  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align\n"));
  else
    {
      printf(_("  Type           Offset             VirtAddr           PhysAddr\n"));
      printf(_("                 FileSiz            MemSiz              Flags  Align\n"));
    }
}

for (i = 0, segment = filedata->program_headers;
       i < filedata->file_header.e_phnum;
       i++, segment++)
{
  if (do_segments)
  {
    printf ("  %-14.14s ", get_segment_type (filedata, segment->p_type));

    if (is_32bit_elf)
    {
      printf ("0x%6.6lx ", (unsigned long) segment->p_offset);
      printf ("0x%8.8lx ", (unsigned long) segment->p_vaddr);
      printf ("0x%8.8lx ", (unsigned long) segment->p_paddr);
      printf ("0x%5.5lx ", (unsigned long) segment->p_filesz);
      printf ("0x%5.5lx ", (unsigned long) segment->p_memsz);
      printf ("%c%c%c ",
        (segment->p_flags & PF_R ? 'R' : ' '),
        (segment->p_flags & PF_W ? 'W' : ' '),
        (segment->p_flags & PF_X ? 'E' : ' '));
        printf ("%#lx", (unsigned long) segment->p_align);
     }

  }
}

出力の形式等は大体わかりました。
あとは愚直に実装していくだけです。


実装

今回は

  • 構造体の定義
  • 構造体のメンバをバイト毎に定義
  • 出力関数の定義

という順番です。
これも前回と一緒ですね。

type ProgramHeader struct {
    Type         Elf64_Word
    Flags        Elf64_Word
    Offset       Elf64_Off
    VirtualAddr  Elf64_Addr
    PhysicalAddr Elf64_Addr
    SegmentSize  Elf64_Xword
    MemorySize   Elf64_Xword
    Alignment    Elf64_Xword
}

構造体のメンバは分かりやすいように長めにしています。

main関数で読み込んだファイルのバイト列を渡し、
解析関数にぶっこんでいます。

b, err := elf.Prepare() //ファイルを読み込んでバイト列を返す
if err != nil {
  return err
}
header := elf.AssignHeader(b[:64]) //ELFヘッダを宣言
pHeaders := elf.AssignProgramHeader(
  b, header.ProgramHeader, header.ProgramHeaderNum, header.ProgramHeaderSize
  ) //プログラムヘッダ解析に必要な情報を渡している
fmt.Printf("Elf file type is %s\n", elf.ParseFileType(header.FileType))
fmt.Printf("Entry point %#x\n", header.EntryPoint)
fmt.Printf("There are %d program headers, starting at offset %d\n", header.ProgramHeaderNum, header.ProgramHeader)
elf.DumpPHeader(pHeaders, header.MachineArchitecture) //出力関数

Goは実装がシンプルになっていいですね。

肝心な解析関数を見ていきましょう。

func AssignProgramHeader(binaries []byte, pHOff Elf64_Off, pHNum, pHSize Elf64_Half) []ProgramHeader {
    binaries = binaries[int(pHOff):] //プログラムヘッダテーブルの始まり以前は切り取る
    phdrs := make([]ProgramHeader, pHNum)
    for i := range phdrs {
        phdrs[i].Type = Elf64_Word(binary.LittleEndian.Uint32(binaries[0:4]))
        phdrs[i].Flags = Elf64_Word(binary.LittleEndian.Uint32(binaries[4:8]))
        phdrs[i].Offset = Elf64_Off(binary.LittleEndian.Uint64(binaries[8:16]))
        phdrs[i].VirtualAddr = Elf64_Addr(binary.LittleEndian.Uint32(binaries[16:24]))
        phdrs[i].PhysicalAddr = Elf64_Addr(binary.LittleEndian.Uint32(binaries[24:32]))
        phdrs[i].SegmentSize = Elf64_Xword(binary.LittleEndian.Uint64(binaries[32:40]))
        phdrs[i].MemorySize = Elf64_Xword(binary.LittleEndian.Uint64(binaries[40:48]))
        phdrs[i].Alignment = Elf64_Xword(binary.LittleEndian.Uint64(binaries[48:56]))
        binaries = binaries[int(pHSize):] //プログラムヘッダのサイズで切り取る
    }
    return phdrs
}

これは愚直に代入しまくるだけなので簡単ですね。

あとは先述したように出力をしていくだけです。

出力には

github.com

を用いています。
キレイなので。

func DumpPHeader(phdrs []ProgramHeader, ma Elf64_Half) {
    t := tablewriter.NewWriter(os.Stdout) //ファイルディスクリプタを指定
    fmt.Println("\nProgram Headers:")
    t.Append(
        []string{
            fmt.Sprintf("%s", aurora.Bold(aurora.Blue("Type"))),
            fmt.Sprintf("%s", aurora.Bold(aurora.Blue("Offset"))),
            fmt.Sprintf("%s", aurora.Bold(aurora.Blue("VirtAddr"))),
            fmt.Sprintf("%s", aurora.Bold(aurora.Blue("PhysAddr"))),
            fmt.Sprintf("%s", aurora.Bold(aurora.Blue("FileSiz"))),
            fmt.Sprintf("%s", aurora.Bold(aurora.Blue("MemSiz"))),
            fmt.Sprintf("%s", aurora.Bold(aurora.Blue("Flags"))),
            fmt.Sprintf("%s", aurora.Bold(aurora.Blue("Align"))),
        })
    for _, phdr := range phdrs {
        t.Append( //バッファに格納する
      []string{
      parsePType(phdr.Type),  //出力が単純ではないので別で関数定義
      fmt.Sprintf("%#x", phdr.Offset), 
      fmt.Sprintf("%#x", phdr.VirtualAddr), 
      fmt.Sprintf("%#x", phdr.PhysicalAddr), 
      fmt.Sprintf("%#x", phdr.SegmentSize), 
      fmt.Sprintf("%#x", phdr.MemorySize), 
      ParseFlags(phdr.Flags), 
      fmt.Sprintf("%#x", phdr.Alignment)})
    }
    t.Render() //バッファをフラッシュして標準出力
}

parsePType,ParseFlagsも見せておきます。

const (
    PT_NULL         = 0          /* Program header table entry unused */
    PT_LOAD         = 1          /* Loadable program segment */
    PT_DYNAMIC      = 2          /* Dynamic linking information */
    PT_INTERP       = 3          /* Program interpreter */
    PT_NOTE         = 4          /* Auxiliary information */
    PT_SHLIB        = 5          /* Reserved */
    PT_PHDR         = 6          /* Entry for header table itself */
    PT_TLS          = 7          /* Thread-local storage segment */
    PT_NUM          = 8          /* Number of defined types */
    PT_LOOS         = 0x60000000 /* Start of OS-specific */
    PT_GNU_EH_FRAME = 0x6474e550 /* GCC .eh_frame_hdr segment */
    PT_GNU_STACK    = 0x6474e551 /* Indicates stack executability */
    PT_GNU_RELRO    = 0x6474e552 /* Read-only after relocation */
    PT_LOSUNW       = 0x6ffffffa
    PT_SUNWBSS      = 0x6ffffffa /* Sun Specific segment */
    PT_SUNWSTACK    = 0x6ffffffb /* Stack segment */
    PT_HISUNW       = 0x6fffffff
    PT_HIOS         = 0x6fffffff /* End of OS-specific */
    PT_LOPROC       = 0x70000000 /* Start of processor-specific */
    PT_HIPROC       = 0x7fffffff /* End of processor-specific */
    PF_R            = 0x4
    PF_W            = 0x2
    PF_X            = 0x1
)

func parsePType(ptype Elf64_Word) string {
    switch ptype {
    case PT_NULL:
        return "NULL"
    case PT_LOAD:
        return "LOAD"
    case PT_DYNAMIC:
        return "DYNAMIC"
    case PT_INTERP:
        return "INTERP"
    case PT_NOTE:
        return "NOTE"
    case PT_SHLIB:
        return "SHLIB"
    case PT_PHDR:
        return "PHDR"
    case PT_TLS:
        return "TLS"
    case PT_GNU_EH_FRAME:
        return "GNU_EH_FRAME"
    case PT_GNU_STACK:
        return "GNU_STACK"
    case PT_GNU_RELRO:
        return "GNU_RELRO"
  default:
    return "NOT IMPLEMENTED YET"
    }
    return "ILLEGAL"
}

func ParseFlags(flags Elf64_Word) string { //ビットのAND演算で判定
    var flag string
    if flags&PF_R != 0 {
        flag += "R"
    } else {
        flag += " "
    }
    if flags&PF_W != 0 {
        flag += "W"
    } else {
        flag += " "
    }
    if flags&PF_X != 0 {
        flag += "E"
    } else {
        flag += " "
    }
return flag

お疲れ様でした!
ここまで実装できれば、
下記のような出力が可能になります!

f:id:orangebladdy:20190423185543j:plain


総評

プログラムヘッダテーブルの解析を実装することで、
ELFフォーマットの理解を深めました。

バイナリ何もわからない人に送る"ソースコード視点"のELFヘッダ解説

目次

  • 目次
  • 概要
  • まずはおさらい
  • 本題:readelf
  • 総評

概要

ELFフォーマットを理解するための試み第二弾。
こちらの記事の続きと言ってもいいです。

drumato.hatenablog.com

上記の記事には間違いが多いので注意してください。
本記事バイナリ何もわからない人に送るELFヘッダ解説が正確です。

自作readelfの完成度を高める為にも、
単純にELFを勉強する教材として優れているという事からも、
本家readelfのコードリーディングは適切だと考えました。

今回は-hオプションに限定していきます。

続きを読む

exec関数で作るMarkdown内スニペット実行のアイディア

目次

  • 目次
  • 概要
  • 前提知識
    • exec関数
  • 本題
  • 総評
    • 注意点

概要

ブログを書く立場上、
プログラムとその実行結果を見せながら解説するというケースはよく発生します。

例えば、

n = 1
print(['even','odd'][n%2])
n = 0
print(['even','odd'][n%2])
#実行結果
odd
even

という場合です。
この時

# コメントアウトで実行結果を示す
n = 1
print(['even','odd'][n%2]) #=>odd
n = 0
print(['even','odd'][n%2]) #=>even

というがありますし、
先に示したようにプログラムと実行結果を分けるというのも考えられます。

しかしこれらに共通して言える事は、
説明する文章とプログラムが別々のファイルであるということです。

今回はMarkdown等の文章にプログラムを記述し、
言語に組み込まれているexec()(系の)関数を利用して
分かりやすいドキュメントを作る方法を考えます。

今回はPythonを例に考えます。

続きを読む

readelf -hの簡易版・省略版を作成するミニ記事

目次

  • 目次
  • 概要
  • 本題:ELFヘッダを見る
  • 本題2:GoでELFヘッダを解析する
  • 総評

概要

コンパイラ自作をとても楽しくやっている私ですが、
どうせコンパイラを作るならバイナリ生成までやらせたいものです。

もっと具体的に言うと、
アセンブリから機械語を生成するアセンブラ( ソフトウェアとしての )を作ってみたいですよね。

アセンブラ自作への欲求はコンパイラ自作を楽しんでる人たちで共有していると思いますが、
これはかなり難しくて、少なくとも次の知識が 必須 になります。
必要な知識のごく一部のみを取り上げます。

まずはELFの理解を深めようということで、
readelfの自作を始めました。

readelfLinuxで用いることができるコマンドで、
elfフォーマットのファイルを見やすく表示したり、
いろんな情報を簡単に見ることができるというものです。

一番有名なのは

$ readelf -h hello

のように、
ELFヘッダを解析するオプション-hを付けた出力ですね。

この ELFヘッダ解析 に焦点を当てて、
ミニ記事ではありますがお話をしていきたいと思います。

今回用いているコードは

github.com

elf/以下に置いてあるので参考までに。

続きを読む

gccの吐くアセンブリをgoccのそれと比較する

目次

  • 目次
  • 概要
  • 本題
    • 最もシンプルなmain関数
    • 変数宣言・四則演算
    • サブルーチンの定義・呼び出し
  • 総評

概要

gccの吐き出すアセンブリを読んでいく記事です。

具体的には gocc で現在実装できている部分までを
実際にgccに吐かせる事で、
現在デファクトとされているCコンパイラとの差異を詳しく見る必要があるという前提の元執筆しました。

このコンパイラは最適化の実装がまだされていないために、  
中間表現、アセンブリには冗長なものが含まれます。  
細かい部分は追わずに、大意を汲み取るように。
続きを読む

1文字変数の確保・演算をgdbで

目次

  • 目次
  • 概要
  • 本題
    • gdb-pedaを使う
  • 総評

概要

ここらへんの記事の続きです。

drumato.hatenablog.com

drumato.hatenablog.com

今回はデバッガの gdbを使いながら、

a=2;b=3;return a*b;

というC( といっていいか微妙ですが) のコードをコンパイルして、
アセンブリの挙動がどうなっていくかについて解説していきます。

リポジトリはいつもどおり

github.com

に。

このコンパイラは最適化の実装がまだされていないために、
中間表現、アセンブリには冗長なものが含まれます。
細かい部分は追わずに、大意を汲み取るように。

続きを読む

コンパイラが構文解析してからアセンブリを出力するまで。

目次

概要

この前上げたこちらの記事について、
より深く解説をしていく記事です。

drumato.hatenablog.com

具体的には、

字句解析後からアセンブリ出力までのフローをより具体的に

return 2+3*4;のケースについて説明していこうと思います。

コンパイラの動きがよくわからないという方には参考になると思います。

goccのリポジトリを貼っておきます。

続きを読む