【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

ELFの動的リンク(2)

前回はELFファイルの構造を示しながら、動的リンクに必要な処理を説明しました。

今回はPythonで実装した簡易ローダを見ながら、実際の処理を追っていきます。

32bit Windows上でELFファイルを読み込み、動的リンクを解決して実行します。Pythonに標準で付属するctypesモジュールを使えば簡単にバイナリ処理できます。

ファイル読み込み

指定されたファイルを読み込みます。省略した場合は a.out を対象とします。

aout = "a.out" if len(argv) < 2 else argv[1]
with open(aout, "rb") as f:
    elf = f.read()

本来は必要な部分だけ読み込むべきですが、プログラムを単純にするため一気に全体を読んでいます。

ELFヘッダ

ファイルがELFヘッダのサイズ以上で、ファイルの先頭がELFシグネチャかどうかを確認します。

assert len(elf) >= 52,        "not found: ELF32 header"
assert elf[0:4] == "\x7fELF", "not found: ELF signature"

ELFファイルが32bitでリトルエンディアンを対象としているかを確認します。詳細は仕様書のe_identを参照してください。

assert ord(elf[4]) == 1,      "not 32bit"
assert ord(elf[5]) == 1,      "not little endian"

ELFヘッダ全体を読みこみます。unpackはstructモジュールの関数で、文字列に格納されたバイナリから値を取り出します。

(e_type,
 e_machine,
 e_version,
 e_entry,
 e_phoff,
 e_shoff,
 e_flags,
 e_ehsize,
 e_phentsize,
 e_phnum,
 e_shentsize,
 e_shnum,
 e_shstrndx) = unpack(
    "<HHLLLLLHHHHHH", elf[16:52])

位置独立コードで、i386を対象としているかを確認します。

assert e_type    == 3, "not PIE"
assert e_machine == 3, "not i386"

今回のローダはWindows上で動きますが、アドレスを指定しても既に使用されていれば確保できないため、どこに読み込んでも良いように位置独立を要求しています。

プログラムヘッダ

ローダが使う情報はプログラムヘッダに書いてあります。

プログラムヘッダは複数あるため、クラスを定義して1つずつインスタンスとして読み込みます。

class Elf32_Phdr:
    def __init__(self, data, pos):
        (self.p_type,
         self.p_offset,
         self.p_vaddr,
         self.p_paddr,
         self.p_filesz,
         self.p_memsz,
         self.p_flags,
         self.p_align) = unpack(
            "<LLLLLLLL", data[pos : pos + 32])

ELFヘッダに指定された情報を基に、プログラムヘッダを読み込みます。

e_phoff プログラムヘッダのファイル中の位置
e_phentsize プログラムヘッダ1個のサイズ
e_phnum プログラムヘッダの個数
phs = [Elf32_Phdr(elf, e_phoff + i * e_phentsize)
       for i in range(e_phnum)]

メモリに読み込み

プログラムヘッダにはファイルからメモリに読み込むときのアドレス(p_vaddr)とサイズ(p_memsz)が書いてあります。位置独立コードでは開始アドレスは0となっているため、終端だけを見て最大値を求めます。

memlen = max([ph.p_vaddr + ph.p_memsz for ph in phs])

必要なサイズの実行可能ページを確保します。最初の引数でアドレスを指定できますが、必ずしも確保できないため、0を指定してWindowsに任せます。戻り値はアドレス(整数値)です。

mem = VirtualAlloc(0, memlen, MEM_COMMIT, PAGE_EXECUTE_READWRITE)

VirtualAllocについてはJITの記事も参照してください。

プログラムヘッダのp_typeがPT_LOADとなっているものだけ、メモリに読み込みます。この単位をセグメントと呼びます。PT_LOAD以外は、どこかのセグメントの中に含まれています。確認のためアドレスを出力します。

pelf = cast(elf, c_void_p).value
for ph in phs:
    addr = mem + ph.p_vaddr
    if ph.p_type == 1: # PT_LOAD
        o, sz = ph.p_offset, ph.p_memsz
        memmove(addr, pelf + o, sz)
        print "LOAD: %08x-%08x => %08x-%08x" % (
            o, o + sz - 1, addr, addr + sz - 1)

.dynamicセクション

プログラムヘッダに.dynamicセクションが指定されていれば、その中を読みます。既にロードされているため、直接メモリから読み込んでいます。.dynamicは type = 0 になるまで読みます。

    elif ph.p_type == 2: # PT_DYNAMIC
        while True:
            type = read32(addr)
            val  = read32(addr + 4)
            if   type ==  0: break
            elif type ==  5: strtab   = mem + val
            elif type ==  6: symtab   = mem + val
            elif type == 11: syment   = val
            elif type == 23: jmprel   = mem + val
            elif type ==  2: pltrelsz = val
            addr += 8

メモリから読み込むread32はctypesを使っています。書き込みのwrite32とセットで定義しています。

def write32(addr, val):
    cast(addr, POINTER(c_uint32))[0] = val
def read32(addr):
    return cast(addr, POINTER(c_uint32))[0]

.rel.pltセクション

jmprelには値が入っていれば.rel.pltセクションが存在します。そこから情報を読み込みます。infoをシフトした値からシンボル情報を取り出します。

if jmprel != None:
    print
    print ".rel.plt(DT_JMPREL):"
    for reladdr in range(jmprel, jmprel + pltrelsz, 8):
        offset = read32(reladdr)
        info   = read32(reladdr + 4)
        stroff = read32(symtab + (info >> 8) * syment)
        name   = string_at(strtab + stroff)
        print "[%08x]offset: %08x, info: %08x; %s" % (
            reladdr, offset, info, name)

シンボル名に合致する関数を探してアドレスを書き込みます。これが実際のリンク作業です。

        assert libc.has_key(name), "undefined reference: " + name
        addr = mem + offset
        faddr = cast(libc[name], c_void_p).value
        print "linking: %s -> [%08x]%08x" % (name, addr, faddr)
        write32(addr, faddr)

本来、シンボル名が指す関数は動的ライブラリの中にあります。しかし今回は処理を単純化するため動的ライブラリは読み込まずに、Pythonで定義した関数を渡しています。

def putchar(ch):
    stdout.write(chr(ch))
    return ch
def puts(addr):
    s = string_at(addr)
    stdout.write(s)
    return len(s)
libc = {
    "putchar": CFUNCTYPE(c_int, c_int)(putchar),
    "puts"   : CFUNCTYPE(c_int, c_void_p)(puts) }

もし本当に動的ライブラリとリンクさせるのであれば、動的ライブラリもメモリに読み込む必要があります。実行可能ファイルと同じようにELFを解析して読み込みます。

テスト

C言語のプログラムをコンパイルします。

void test(char start, const char *str) {
    int i;
    for (i = 0; i < 26; i++)
        putchar(start + i);
    putchar('\n');
    puts(str);
}

void _start() {
    test('A', "Hello, ELF!\n");
    test('a', "done.\n");
}

これを読み込ませれば、無事に実行できました。

===== 00000000-00001397 => 00420000-00421397
LOAD: 00000000-000002fb => 00420000-004202fb
LOAD: 000002fc-00000397 => 004212fc-00421397

.rel.plt(DT_JMPREL):
[004201ac]offset: 00001390, info: 00000107; putchar
linking: putchar -> [00421390]00370fdc
[004201b4]offset: 00001394, info: 00000207; puts
linking: puts -> [00421394]00370fc0

ABCDEFGHIJKLMNOPQRSTUVWXYZ
Hello, ELF!
abcdefghijklmnopqrstuvwxyz
done.

遅延リンク

.pltには遅延リンク用のコードが入っています。今回は実行前にすべてリンクしてしまいましたが、最初に関数を呼び出すときにリンクすることができます。

libcの関数と同じようにPython側のリンク関数にコールバックさせることで実装できます。興味のある方はコードを掲載するので参照してください。

def interp(id, offset):
    print "delayed link: id=%08x, offset=%08x" % (id, offset)
    return link(jmprel + offset)

thunk_interp = CFUNCTYPE(c_void_p, c_void_p, c_uint32)(interp)
call_interp = JIT([
    0xff, 0x14, 0x24, # call [esp]
    0x83, 0xc4, 8,    # add esp, 8
    0x85, 0xc0,       # test eax, eax
    0x74, 2,          # jz 0f
    0xff, 0xe0,       # jmp eax
    0xc3 ])           # 0: ret
if pltgot != None:
    writeptr(pltgot + 4, thunk_interp)
    writeptr(pltgot + 8, call_interp)

64bit

64bitではポインタのサイズが異なりますが、ELFの構造自体は同じです。しかしWindowsとUNIX系ではABI(関数呼び出し時のレジスタの使い方など)が異なるため変換する必要があります。

ABI 引数 保存するレジスタ
Windows rcx, rdx, r8, r9 rbx, r12-r15, rdi, rsi
UNIX rdi, rsi, rdx, rcx, r8, r9 rbx, r12-r15

Windowsでは4つの引数分のスタックを空けておく必要がありますが、UNIXではその必要はありません。詳細は以下の資料を参照してください。

変換を実装してWindowsでSystem V ABIの64bitコードを読み込めるようにしたローダを掲載します。ABIを変換している部分を抜粋します。

def SYSV2WIN64(restype, *argtypes):
    assert len(argtypes) <= 4, "too long arguments"
    def init(f):
        ret = JIT([
            0x48, 0xb8] + [0]*8 + [ # movabs rax, addr
            0x49, 0x89, 0xc9,       # mov r9 , rcx
            0x49, 0x89, 0xd0,       # mov r8 , rdx
            0x48, 0x89, 0xf2,       # mov rdx, rsi
            0x48, 0x89, 0xf9,       # mov rcx, rdi
            0x48, 0x83, 0xec, 0x28, # sub rsp, 40
            0xff, 0xd0,             # call rax
            0x48, 0x83, 0xc4, 0x28, # add rsp, 40
            0xc3 ])                 # ret
        ret.f = CFUNCTYPE(restype, *argtypes)(f)
        writeptr(ret.addr + 2, ret.f)
        return ret
    return init

終わりに

以上でELFの動的リンクの説明は終了です。

ローダやリンカを自作する一助になることを祈っています。