LoginSignup
15
11

More than 3 years have passed since last update.

ELF形式のヘッダ部分を解析する単純なプログラムを作ってみた。

Last updated at Posted at 2019-11-12

ELF形式のヘッダ部分を解析する単純なプログラムを作ってみた。

こんにちは、にわかです。
ELFのヘッダ部分を解析する単純なプログラムを作ってみました。(ELF形式の勉強のついでとして)
セグメント情報とエントリポイントだけをターミナルに表示するだけの単純なプログラムです。
readelfコマンドで解析したらいいじゃんという意見もあると思うのですが、自分で作ってみる方が理解が深まると思ったんです。(リアルがうまくいかないので、たまに現実逃避して低レイヤの勉強をして気を紛らわしてます。)

ELF形式のOSを起動をリアルモードから

プログラム自体は、最後に載せているのでみたい方はそちらに飛んでください。

ELFとは

ELF(Executable and Linkable Format)は、Linuxでデフォルトになっているオブジェクトファイルと実行ファイルのフォーマトです。

EXEフォーマットの仲間みたいなもんです。

実行ファイルもオブジェクトファイルもどちらもELFフォーマットとして扱うことができるのが特徴の1つです。

セグメント(コード領域とデータ領域について)

セグメントには大きく分けて2種類のセグメントがあります。
1つ目が、コードセグメント
2つ目が、データセグメント(dataセグメントとbssセグメントなどがある。)

コードセグメントには、CPUが実行する機械語命令が格納されています。

データセグメントには、グローバル変数やstatic変数が格納されています。ローカル変数は格納されていません。また、データセグメントには、大きく分けて2種類存在します。dataセグメントとbssセグメントです。
dataセグメントには、初期化したstatic変数やグローバル変数を配置します。
bssセグメントには、初期化されていないstatic変数やグローバル変数を配置します。

OSはセグメントという単位でメモリ管理を行なっていて、ELFのヘッダには各セグメント情報が記述されています。なので、ヘッダ部分を見て、OSがRAMに配置します。

OSはアプリケーションをRAMにロードする際、ヘッダ部分を参考にしながらプログラムをロードします。その際、セグメント単位でRAMにロードをします。

BSS領域は、ファイルには含まれていません。(BSSに関するヘッダ情報はありますよ!!)なので、BSSのファイルサイズは0byteです。OSが、BSS領域をRAM上に確保してその領域を0で初期化するなり何かしらの値で初期化します。(初期値はOS依存)
BSSがファイルの中で実体として存在しない本当の理由は知りませんが、初期化していないわけですし、データとして保持する必要はないという理由じゃないかなあと思っています。

ローカル変数はプログラム実行開始してから関数が呼び出されて、初めてローカル変数がスタックに積まれるので、ファイルには変数の実体は存在しません。(ローカル変数をスタックに詰むという命令はファイルに存在してますよ。変数自体がファイルにないということです。)

ELFヘッダ

ELFヘッダには、ファイルの中身がビッグインディアンなのか?リトルインディアンなのか?、実行ファイルの場合はエントリポイント(プログラムの開始番地)なども書かれています。
ファイル全体に関係する情報がELFヘッダには含まれています。
ちなみにx86系CPUはリトルインディアンです。

ELFヘッダの構造体を下に示します。

typedef struct elf_header{
   char e_ident[16];
   short e_type;
   short e_machine;
   int e_version;
   int e_entry;
   int e_phoff;
   int shoff;
   int e_flags;
   short e_ehsize;
   short e_pehtsize;
   short e_phnum;
   short e_shetsize;
   short e_shnum;
   short e_shstrndx;
}ELF_HEADER;

上の構造体で、今回の解析プログラムで使用する要素は、e_entryとe_phoffです。

e_entryは、プログラムの開始番地を示しています。
e_phoffは、プログラムヘッダのファイルの先頭からのオフセットを示しています。

以下にELF形式の簡単な構成を示します。(大雑把な図です。)

image.png
(図1)

プログラムヘッダ

プログラムヘッダには、セグメントの情報が含まれています。例えば、ファイル上でのサイズとかメモリ上でのサイズとか、開始論理番地などが含まれています。

プログラムヘッダは、一つのセグメントにつき1つ必ず存在します。(1対1)
(なので、セグメントヘッダの方が名前としていいのでは?と思っています。)
普通、実行形式は複数のセグメントで構成されていますので、プログラムヘッダは複数あるということになりますよね。
ということで、ELFのe_phoffがさす領域には、複数のプログラムヘッダが存在します。つまりプログラムヘッダの配列が、ELFの示すオフセットからズラーと並んでいるんです。(図1をもう一度眺めてください。)
OSがロードするために必要な情報なので、オブジェクトファイルに関しては、プログラムヘッダが存在しません。
以下にプログラムヘッダの構造体を示します。

typedef struct ph_header{
   int p_type;
   int p_offset;
   int p_vaddr;
   int p_paddr;
   int filesz;
   int p_memmsz;
   int p_flags;
   int p_align;
}PH_HEADER;

上の構造体を見ると、プログラムヘッダは32byteになっていることが分かります。
上の構造体が配列状にズラーと並んでいるんです。(構造体の配列)

PH_HEADER構造体で、自作解析プログラムで使用する要素は
p_vaddrとp_offsetとfileszです。

p_vaddrは、論理アドレス(IntelのCPUでは、これをリニアアドレスと呼ぶという決まり)
p_offsetは、今注目しているプログラムヘッダに対応するセグメントのファイルからのオフセットが格納されています。
fileszは、今注目しているプログラムヘッダに対応するセグメントのファイルサイズです。(BSSの場合は0)

自分が用意したリンカスクリプトについて

簡単なリンカスクリプトを用意しました。

下のリンカスクリプトを見ていただくと分かりますが、
ロードするためのプログラムヘッダは3種類用意されることになります。
(BSS領域に関してはロードしませんが、RAMに必要な分だけの確保はします。)

test.ls
ENTRY(main);

SECTIONS{
    . = 0x0c000000 + SIZEOF_HEADERS;
    .text : {*(.text)}
    . = 0x0c004000;
    .data : {*(.data)*(.rodata*)}
    . = 0x0c006000;
    .bss  : {*(.bss)}
}


各セグメントを0x0c000000番地以降に配置しているのは、気まぐれで決めました。

ELF形式のヘッダ部分を解析する単純なプログラム

print_elf.c
#include<stdio.h>
#include<stdlib.h>


typedef struct elf_header{
   char e_ident[16];
   short e_type;
   short e_machine;
   int e_version;
   int e_entry;
   int e_phoff;
   int shoff;
   int e_flags;
   short e_ehsize;
   short e_pehtsize;
   short e_phnum;
   short e_shetsize;
   short e_shnum;
   short e_shstrndx;
}ELF_HEADER;

typedef struct ph_header{
   int p_type;
   int p_offset;
   int p_vaddr;
   int p_paddr;
   int filesz;
   int p_memmsz;
   int p_flags;
   int p_align;
}PH_HEADER;

//プログラムヘッダ表示
void print_programheader(PH_HEADER *Prog_Header){
    int p_type;
    static int index=1;
    p_type = Prog_Header->p_type;

    if(p_type!=1){
        return;
    }
    printf("\n");
    printf("%d : segment\n", index);

    printf("p_ype        : %04x\n",  Prog_Header->p_type);
    printf("FileOffset    : %08x\n", Prog_Header->p_offset);
    printf("VirAddr       : %08x\n", Prog_Header->p_vaddr);
    printf("FileSize      : %08x\n", Prog_Header->filesz);
    printf("\n");

    index=index+1;
    print_programheader(Prog_Header+1);
}

//メイン関数
int main(int argc, char *argv[]){
    FILE *elf_file;
    unsigned char *buff;
    ELF_HEADER *Elf_Header;
    PH_HEADER *Prog_Header;


    //8192バイト分確保する。
    //8192バイトの大きさには、意味はないです。とりあえずこんくらい確保すれば十分かな
    buff = malloc(sizeof(char)*8192);

    elf_file = fopen(argv[1], "rb");

    printf("\n\n%s\n\n", argv[1]);
    if(elf_file==NULL){
        printf("ファイルを開けませんでした。");
        return 0;
    }

    //buffにelf_fileのファイル内容を展開する。
    fread(buff, 1, 8192, elf_file);
    Elf_Header = buff;

    //ELFヘッダ
    printf("EntryPoint    : %08x\n", Elf_Header->e_entry);
    //プログラムヘッダーへのオフセット分カウントする。
    Prog_Header = buff + Elf_Header->e_phoff;

    //プログラムヘッダ
    print_programheader(Prog_Header);
    fclose(elf_file);

    return 1;
}

void print_programheader(PH_HEADER *Prog_Header)関数の終了条件の意味は、
p_typeが1の時、ロードする必要のあるセグメントを示しています。
1じゃない時に終了するようにしました。(用意したリンカスクリプトの3つのセグメントは、先頭から連続して並んでいる。)

    //終了条件
    if(p_type!=1){
        return;
    }

追記
プログラムヘッダが4つ以上ある前提の終了条件になっていて、もしかしたら3つしか用意されていないのかもしれません。
普通に、index=4を終了条件にしたらいい気がします。

プログラムが正しく動作しているのかの実験

とりあえず、このプログラムがちゃんと動いているのかをチェックするためにreadelfコマンドと比較してみることにしました。
実験環境は、
MacOS、x86用にビルドされたgcc, binutils(後述)です。

まずは、gccとbinutilsをインストールします。

brew install i386-elf-gcc
brew install i386-elf-binutils

(i386用のリンカってi386-elf-binutilsをbrewすればインストールされているはずです。。。されてなければ、連絡ください。。。)

次に実験に使用するプログラムを2つ用意します。今回は、test1.cとtest2.cを用意しました。

test1.c
int a=10;
int b;

void main(){
    int c=10;
}
test2.c
int d=10;

これらをコンパイルして、オブジェクトファイルtest1.o、test2.oを生成します。

i386-elf-gcc -c -m32 -fno-pic -o test1.o test1.c
i386-elf-gcc -c -m32 -fno-pic -o test2.o test2.c

次にリンクします。

i386-elf-ld -nostdlib -nostartfiles -e main -o test.bin -T test.ls test1.o test2.o

これで出来上がるELF形式のファイルは、test.binという名前のファイルです。

自作したプログラムで、ヘッダ部分をのぞいてみます。

※print_elf.cをコンパイルする際は、各OS用の実行フォーマットを生成するコンパイラを利用してください!!!!例えば、Windowsの方は、EXEフォーマットを生成するコンパイラで、print_elf.cをコンパイルしてください。

./print_elf test.bin

結果は以下のようになりました。
スクリーンショット 2019-11-12 16.57.32.png

次にreadelfコマンドを利用して、中身をみると

readelf -a test.bin

スクリーンショット 2019-11-12 16.57.51.png

スクリーンショット 2019-11-12 16.58.06.png

自作の解析プログラムがちゃんと動作していることが確認できました。

ちなみに変数a(初期化されているグローバル変数), b(初期化されていないグローバル変数), c(ローカル変数), d(初期化されているグローバル変数)を変数として用意しました。

ということは、各セグメントのファイル上のサイズは
dataセグメント : 8byte
bss セグメント : 0byteとなるはずですよね
(え?という方は、セグメント(コード領域とデータ領域について)という項目をもう一度見てみるといいです。)

dataセグメント(2つ目のセグメント)とbssセグメント(3つ目のセグメント)のファイルサイズ(FileSiz)を確認してみてください。

一致してることがわかります。

最後に

次回は、ELF形式のOSを起動してみたという記事を出そうと思います。

参考文献

下の本はとにかく分かりやすいです。ELFヘッダやプログラムヘッダについてもっと知りたいという方にオススメです。
リンカ・ローダ実践開発テクニック―実行ファイルを作成するために必須の技術

ELFヘッダの構造体の各要素のバイトサイズなんかを参考にしました。ELFとは何か?を知らずに下を見ても訳がわからないと思います。。。。。
Executable and Linking Format (ELF)

15
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
11