Explore "Full-Stack" in depth!

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

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

目次

概要

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

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

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

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

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

一番有名なのは

$ readelf -h hello

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

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

今回用いているコードは

github.com

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


本題:ELFヘッダを見る

まずは実際にreadelf -hコマンドを用いて出力を見てみましょう。

f:id:orangebladdy:20190416224601j:plain

こんな感じの環境で実行しています。

#include<stdio.h>

int main(void){
  printf("Hello,World\n");
  return 0;
}

HelloWorldを書いて、
まずはコンパイルしてみます。

$ gcc -o hello hello.c
$ readelf -h hello

として、実行結果を見てみます。

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x530
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6448 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

上記のように表示されました。

詳しくは扱いませんが、今後の話を理解できる程度に簡単に解説します。

  • Magicマジックナンバー のフィールド。 ファイルを識別する番号だと思えばいい。
  • Class…ELFフォーマットを二分する(Noneを含めると3つ)フィールド。
    • ELF32ELF64がある。
  • Data…これも大まかには2つ(Noneを含めると3つ)に分かれる
    • Big Endian…hex表記の1ワードを上位バイトから順に並べる表記法
    • Little Endian…下位バイトから順に並べる表記法
      • 各バイト内でビット列の並びが変わるわけじゃないので注意
  • Version…基本的に1なので省略
  • OS/ABI…バイナリに埋め込まれたOS,ABIの情報が入っている
  • ABIVersion…これも基本0。省略。
  • Type…バイナリファイルが具体的に何かを示す。
  • Machine…CPUアーキテクチャの情報
  • Version…オブジェクトファイル( hello.o とか)のバージョン
  • Entry point address…プログラム実行時最初に参照される仮想アドレスが格納。
  • Start of program headers…プログラムヘッダの大きさ
    • プログラムヘッダには プログラム実行時に必要な情報が格納されている
    • 詳しくはまた記事にするかも?
  • 以下のヘッダ情報は今回は省略する。

これらはそれぞれ フィールドの大きさ が定義されています。
つまり定義されたバイト長でバイナリを区切っていけばヘッダ解析ができそうです。

より詳しくELFについて知りたい方は、
こちらこちらを参照することをオススメします。


本題2:GoでELFヘッダを解析する

まず今回作成した( 簡易版 ) readelf -hはこちらです。

f:id:orangebladdy:20190417080134j:plain

出てる情報は少ないですが、
先程挙げた本家readelfに近い出力ができているかな、と思います。

それではコードを簡単に紹介します。

まずは

github.com

main.goです。

普段から私の記事を見てくださっている方はわかると思いますが、
このリポジトリはCコンパイラ自作のリポジトリになっています。

CLIが既にできていたので、ここにコードを追加していきました。

package main

import (
    "drum/gocc/elf"
    "drum/gocc/lexer"
    "drum/gocc/parser"
    "fmt"
    "io/ioutil"
    "os"
    "os/exec"
    "path/filepath"

    "github.com/logrusorgru/aurora"
    "github.com/urfave/cli"
)

const (
    readelfFormat = "%s\t\t\t%s\n"
    ErrFormat     = "Error found: %s\n"
    VERSION       = `1.0.0`
    NAME          = `Gocc`
    USAGE         = `A Compiler refered to rui314/9cc-Language`
    AUTHOR        = `Drumato`
    LINK          = `https://github.com/Drumato/gocc`
)

var (
    app = cli.NewApp()
)

func main() {
    if err := app.Run(os.Args); err != nil {
        fmt.Printf(ErrFormat, aurora.Bold(aurora.Red(fmt.Sprintf("%+v", err))))
    }
}

func init() {
    app.Version = VERSION
    app.Name = NAME
    app.Usage = USAGE
    app.Author = AUTHOR
    app.Email = LINK
    app.Flags = []cli.Flag{
        cli.BoolFlag{Name: "dump,d", Usage: "debugging ir"},
    }
    app.Action = func(c *cli.Context) error {
        if err := Start(c); err != nil {
            fmt.Printf(ErrFormat, aurora.Bold(aurora.Red(fmt.Sprintf("%+v", err))))
        }
        return nil
    }
    app.Commands = []cli.Command{
        {
            Name:    "file",
            Aliases: []string{"f", "file"},
            Usage:   "compile with specifying file",
            Action: func(c *cli.Context) error {
                if len(os.Args) < 2 {
                    return fmt.Errorf("%v\n", aurora.Bold(aurora.Red("Please specify an source-code file written by C")))
                }
                if filepath.Ext(os.Args[2]) != ".c" {
                    return fmt.Errorf("%v\n", aurora.Bold(aurora.Red("gocc only supporting .c file!")))
                }
                f, err := os.Open(os.Args[2])
                if err != nil {
                    return err
                }
                b, err := ioutil.ReadAll(f)
                if err != nil {
                    return err
                }
                input := string(b)
                fmt.Println(aurora.Bold(aurora.Blue("now compiling...")))
                l := lexer.New(input, "")
                p := parser.New(l)
                code := parser.GenIR(p.Parse())
                if c.Bool("dump") {
                    for _, fn := range code {
                        fmt.Printf("%+v\n", fn)
                        for _, ir := range fn.IRs {
                            fmt.Printf("%+v\n", ir)
                        }
                    }
                }
                parser.AllocateRegisters(code)
                f, err = os.Create("tmp.s")
                if err != nil {
                    return err
                }
                parser.Genx86(f, code)
                exec.Command("gcc", "-static", "-o", "tmp", "tmp.s").Run()
                return nil
            },
        },
        {
            Name:    "readelf",
            Aliases: []string{"r", "readelf"},
            Usage:   "read ELF fomrat",
            Subcommands: cli.Commands{
                {
                    Name:    "header",
                    Aliases: []string{"h", "header"},
                    Usage:   "read header of ELF fomrat",
                    Action: func(c *cli.Context) error {
                        if len(os.Args) < 3 {
                            return fmt.Errorf("%v\n", aurora.Bold(aurora.Red("Please specify an binary file")))
                        }
                        if filepath.Ext(os.Args[3]) != "" {
                            return fmt.Errorf("%v\n", aurora.Bold(aurora.Red("only supporting binary file!")))
                        }
                        f, err := os.Open(os.Args[3])
                        if err != nil {
                            return err
                        }
                        b, err := ioutil.ReadAll(f)
                        if err != nil {
                            return err
                        }
                        header := elf.ParseHeader([]byte(b)[:52])
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Magic:")), elf.ParseMagicNumber(header.MagicNumber))
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Class:")), elf.ParseClass(header.Class))
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Data:")), elf.ParseData(header.Data))
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Version:")), elf.ParseVersion(header.Version))
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("OS/ABI:")), elf.ParseOSABI(header.OSABI))
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("ABI Version:")), elf.ParseABIVersion(header.ABIVersion))
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Type:")), elf.ParseFileType(header.FileType[:]))
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Machine:")), elf.ParseMachineArchitecture(header.MachineArchitecture[:]))
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Version:")), fmt.Sprintf("%#x", header.FileVersion[:]))
                        fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Entry point address:")), elf.Endian(fmt.Sprintf("%x", header.EntryPoint[:])))
                        // fmt.Println(header.ProgramHeader)
                        // fmt.Println(header.SectionHeader)
                        // fmt.Println(header.Unused)
                        // fmt.Println(header.HeaderSize)
                        // fmt.Println(header.ProgramHeaderSize)
                        // fmt.Println(header.ProgramHeaderNum)
                        // fmt.Println(header.SectionHeaderSize)
                        // fmt.Println(header.SectionHeaderNum)
                        // fmt.Println(header.SectionNumber)
                        return nil
                    },
                },
            },
        },
    }
}

func Start(c *cli.Context) error {
    input := string([]rune(os.Args[1]))
    l := lexer.New(input, "")
    p := parser.New(l)
    code := parser.GenIR(p.Parse())
    if c.Bool("dump") {
        for _, fn := range code {
            fmt.Printf("%+v\n", fn)
            for _, ir := range fn.IRs {
                fmt.Printf("%+v\n", ir)
            }
        }
    }
    parser.AllocateRegisters(code)
    parser.Genx86(os.Stdout, code)
    return nil
}

今回見て欲しいのは、

Action: func(c *cli.Context) error {
    if len(os.Args) < 3 {
      return fmt.Errorf("%v\n", aurora.Bold(aurora.Red("Please specify an binary file")))
    }
    if filepath.Ext(os.Args[3]) != "" {
        return fmt.Errorf("%v\n", aurora.Bold(aurora.Red("only supporting binary file!")))
    }
    f, err := os.Open(os.Args[3])
    if err != nil {
        return err
    }
    b, err := ioutil.ReadAll(f)
    if err != nil {
        return err
    }
    header := elf.ParseHeader([]byte(b)[:52])
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Magic:")), elf.ParseMagicNumber(header.MagicNumber))
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Class:")), elf.ParseClass(header.Class))
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Data:")), elf.ParseData(header.Data))
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Version:")), elf.ParseVersion(header.Version))
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("OS/ABI:")), elf.ParseOSABI(header.OSABI))
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("ABI Version:")), elf.ParseABIVersion(header.ABIVersion))
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Type:")), elf.ParseFileType(header.FileType[:]))
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Machine:")), elf.ParseMachineArchitecture(header.MachineArchitecture[:]))
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Version:")), fmt.Sprintf("%#x", header.FileVersion[:]))
    fmt.Printf(readelfFormat, aurora.Bold(aurora.Blue("Entry point address:")), elf.Endian(fmt.Sprintf("%x", header.EntryPoint[:])))
    // fmt.Println(header.ProgramHeader)
    // fmt.Println(header.SectionHeader)
    // fmt.Println(header.Unused)
    // fmt.Println(header.HeaderSize)
    // fmt.Println(header.ProgramHeaderSize)
    // fmt.Println(header.ProgramHeaderNum)
    // fmt.Println(header.SectionHeaderSize)
    // fmt.Println(header.SectionHeaderNum)
    // fmt.Println(header.SectionNumber)
    return nil
},

これが実際に出力している処理となります。

次にelfパッケージを見ていきましょう。

package elf

import (
    "os"

    "github.com/sirupsen/logrus"
)

type ElfHeader struct {
    MagicNumber         [16]byte
    Class               byte
    Data                byte
    Version             byte
    OSABI               byte
    ABIVersion          byte
    Padding             [7]byte
    FileType            [2]byte
    MachineArchitecture [2]byte
    FileVersion         [4]byte
    EntryPoint          [4]byte
    ProgramHeader       [4]byte
    SectionHeader       [4]byte
    Unused              [4]byte
    HeaderSize          [2]byte
    ProgramHeaderSize   [2]byte
    ProgramHeaderNum    [2]byte
    SectionHeaderSize   [2]byte
    SectionHeaderNum    [2]byte
    SectionNumber       [2]byte
}

func ParseHeader(binaries []byte) *ElfHeader {
    if len(binaries) != 52 {
        logrus.Errorf("Invalid format")
        os.Exit(1)
    }
    head := &ElfHeader{}
    indices := []int{4, 5, 6, 7, 8, 9, 16, 18, 20, 24, 28, 32, 36, 40, 42, 44, 46, 48, 50, 52}
    elements := [][]byte{}
    past := 0
    for i, idx := range indices {
        if i == 0 {
            past = 0
            elements = append(elements, binaries[0:16])
        } else {
            elements = append(elements, binaries[past:idx])
        }
        past = idx
    }
    copy(head.MagicNumber[:], elements[0])
    head.Class = byte(elements[1][0])
    head.Data = byte(elements[2][0])
    head.Version = byte(elements[3][0])
    head.OSABI = byte(elements[4][0])
    head.ABIVersion = byte(elements[5][0])
    copy(head.Padding[:], elements[6])
    copy(head.FileType[:], elements[7])
    copy(head.MachineArchitecture[:], elements[8])
    copy(head.FileVersion[:], elements[9])
    copy(head.EntryPoint[:], elements[10])
    copy(head.ProgramHeader[:], elements[11])
    copy(head.SectionHeader[:], elements[12])
    copy(head.Unused[:], elements[13])
    copy(head.HeaderSize[:], elements[14])
    copy(head.ProgramHeaderSize[:], elements[15])
    copy(head.ProgramHeaderNum[:], elements[16])
    copy(head.SectionHeaderSize[:], elements[17])
    copy(head.SectionHeaderNum[:], elements[18])
    copy(head.SectionNumber[:], elements[19])
    return head
}

func Endian(s string) string {
    var ret string
    for i := len(s); i > 0; i -= 2 {
        if s[i-2:i] == "00" {
            continue
        }
        ret += s[i-2 : i]
    }
    return "0x" + ret[:len(ret)]
}

ELFヘッダの構造体を定義して、
愚直に代入しまくっています。

エンディアンの関数はGoの標準パッケージがサポートしていますが、
今回はそれっぽいのを自作しました。 おそらく間違ってます。

あとは実際に区切ったバイトごとに、
出力を細かく分けるだけです。


総評

ELFヘッダの本当に基礎的な勉強ができてよかったかな、と思いました。
より詳しく勉強していくなかで、
同時並行的にこのreadelfもどきを改造していけたらなと思います。