readelf -hの簡易版・省略版を作成するミニ記事
目次
注意
readelf 実装
で検索すると未だに一番上に出てきてしまっているので注意.
これはelfについて全然詳しくない時期に書いたものです.
一応入門的内容についてまとめたものがあるので,
よろしければそちらを.
概要
コンパイラ自作をとても楽しくやっている私ですが、
どうせコンパイラを作るならバイナリ生成までやらせたいものです。
もっと具体的に言うと、
アセンブリから機械語を生成するアセンブラ( ソフトウェアとしての )を作ってみたいですよね。
アセンブラ自作への欲求はコンパイラ自作を楽しんでる人たちで共有していると思いますが、
これはかなり難しくて、少なくとも次の知識が 必須 になります。
必要な知識のごく一部のみを取り上げます。
まずはELFの理解を深めようということで、
readelf
の自作を始めました。
readelf
はLinuxで用いることができるコマンドで、
elfフォーマットのファイルを見やすく表示したり、
いろんな情報を簡単に見ることができるというものです。
一番有名なのは
$ readelf -h hello
のように、
ELFヘッダを解析するオプション-h
を付けた出力ですね。
この ELFヘッダ解析 に焦点を当てて、
ミニ記事ではありますがお話をしていきたいと思います。
今回用いているコードは
https://github.com/drumato/goccgithub.com
のelf/
以下に置いてあるので参考までに。
本題:ELFヘッダを見る
まずは実際にreadelf -h
コマンドを用いて出力を見てみましょう。
こんな感じの環境で実行しています。
#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
… マジックナンバー のフィールド。 ファイルを識別する番号だと思えばいい。7f 45 4c 46
は ELFバイナリのマジックナンバー。- その後は別の情報として利用される。
Class
…ELFフォーマットを二分する(Noneを含めると3つ)フィールド。ELF32
とELF64
がある。
Data
…これも大まかには2つ(Noneを含めると3つ)に分かれるBig Endian
…hex表記の1ワードを上位バイトから順に並べる表記法Little Endian
…下位バイトから順に並べる表記法- 各バイト内でビット列の並びが変わるわけじゃないので注意。
Version
…基本的に1なので省略OS/ABI
…バイナリに埋め込まれたOS,ABIの情報が入っている- ABIとは Application Binary Interface の略。
- バイナリレベルで互換性を保証するインタフェースのこと
ABIVersion
…これも基本0。省略。Type
…バイナリファイルが具体的に何かを示す。Machine
…CPUアーキテクチャの情報Version
…オブジェクトファイル(hello.o
とか)のバージョンEntry point address
…プログラム実行時最初に参照される仮想アドレスが格納。Start of program headers
…プログラムヘッダの大きさ- プログラムヘッダには プログラム実行時に必要な情報が格納されている
- 詳しくはまた記事にするかも?
- 以下のヘッダ情報は今回は省略する。
これらはそれぞれ フィールドの大きさ が定義されています。
つまり定義されたバイト長でバイナリを区切っていけばヘッダ解析ができそうです。
本題2:GoでELFヘッダを解析する
まず今回作成した( 簡易版 ) readelf -h
はこちらです。
出てる情報は少ないですが、
先程挙げた本家readelfに近い出力ができているかな、と思います。
それではコードを簡単に紹介します。
まずは
https://github.com/Drumato/goccgithub.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もどきを改造していけたらなと思います。