unite.vimでカラースキームの色相を変化させる
huerotation.vim素晴らしいですね.でもどうせだったらunite-colorschemeのように変化の様子が見たいですよね.
ということで作りました.ちゃんと独立してunite.vimのソースを作ってもいいんですが,100行にも満たないでしょうから.vimrcに直接書くことにします(unite.vimは.vimrcで自分のソース等を追加することができます).
これを.vimrcに記述すれば :Unite -auto-preview rotate_hue で実行できます(-auto-previewは任意).特にたいしたことはしていませんがRotateHue()そのままでは色相のプレビューができないためs:rotate_hue()で動作をラップしています.またこの関数は.vimrcローカルな関数なので,アクションとして利用するためにs:convertLocalFunc()で外部からも呼べよう関数名を変換しています*1.当然ながらhuerotation.vimが必要です.また,候補の間隔は引数で変えることができます(例 :Unite -auto-preview rotate_hue:40).デフォルトは20です.
実行の様子
これでユナイトローテートビューティフルアタックができますね!!!
*1:s:convertLocalFunc()はどっからか取ってきたんだけどどこだったっけ..
listingsでスペースを省略させない
texでソースコード貼り付けにlistingsを使うとソースコード中の複数個の空白が1個の空白に置き換えられてしまって困っていたけど次のオプションを設定してあげればよかった.
\lstest{keepspaces=true}
まぁ,ちゃんとマニュアルは読みましょうという話.
CTAN: /tex-archive/macros/latex/contrib/listings
Mac OS X で lcc
Mac OS X で lcc (lcc, A Retargetable Compiler for ANSI C) を使う方法
普通にビルドできます
$ git clone https://github.com/drh/lcc.git $ cd lcc $ mkdir build $ export BUILDDIR=./build $ make rcc $ build/rcc -target=x86/linux tst/8q.c
build/rcc が本体.
シンボル間の引き算
再配置情報について調べてたらアセンブリでのシンボル間の引き算についてelf/i386とmach-o/x86-64で挙動が違ったのでめも.
elf/i386 の場合
(実行環境: Ubuntu 10.04LTS (linux 2.6.32), GNU assembler version 2.20.1 (i486-linux-gnu))
まず.シンボル間の足し算はできません(当然)
.data a: .byte 1 b: .byte 2 z: .long b+a
$ gcc -c -o a.o a.s a.s: Assembler messages: a.s:9: Error: invalid sections for operation on `b' and `a'
以下の演算はできます
.data a: .byte 1 b: .byte 2 c: .byte 3 d: .byte 4 u: .long a # もちろんOK. R_386_32な再配置(aのアドレス) x: .long b - a # OK. この場合は再配置は必要ない y: .long c - b - a # OK. この場合は再配置が必要
$ gcc -c -o a.o a.s $ objdump -r a.o ... RELOCATION RECORDS FOR [.data]: OFFSET TYPE VALUE 00000004 R_386_32 .data 0000000c R_386_PC32 *ABS*
最後のyは.dataセクションがロードされるアドレスを引く必要があるため,再配置が必要となります.実際の再配置種別的にはR_386_PC32であるので,yにはaddendとしてc-b-a+yの値が入っています.
$ objdump -D a.o 00000000 <a>: 0: 01 02 add %eax,(%edx) 00000001 <b>: 1: 02 03 add (%ebx),%al 00000002 <c>: 2: 03 04 00 add (%eax,%eax,1),%eax ... 0000000c <y>: c: 0d .byte 0xd # y に入っている値は0x0d d: 00 00 add %al,(%eax) # これはc-b-a+y
異なるセクション間のシンボルの引き算はできません
.text a: .byte 1 .data b: .byte 2 z: .long b-a
$ gcc -c -o a.o a.s a.s: Assembler messages: a.s:10: Error: can't resolve `.data' {.data section} - `a' {.text section}
Mach-O/x86-64 の場合
(実行環境:Mac OS X 10.8,Apple Inc version cctools-836, GNU assembler version
1.38)
シンボルの足し算ができないのは同じです.
引き算は一つまで.
.data a: .quad 1 b: .quad 2 c: .quad 3 x: .quad a # もちろんOK. # X86_64_RELOC_UNSIGNED な再配置 y: .quad b - a # OK. mach-oでは再配置が必要 # (X86_64_RELOC_SUBTRACTORとX86_64_RELOC_UNSIGNED)
% gcc -c -o a.o a.s % otool -rv a.o z.o: Relocation information (__DATA,__data) 3 entries address pcrel length extern type scattered symbolnum/value 00000020 False quad True SUB False a 00000020 False quad True UNSIGND False b 00000018 False quad True UNSIGND False a
3つの引き算はできません.
.data a: .quad 1 b: .quad 2 c: .quad 3 z: .quad c - b - a # できない
% gcc -c -o z.o z.s z.s:8:Expression too complex, 2 symbols forgotten: "b" "a"
異なるセクション間のシンボルの引き算はできます(!)
.text a: .quad 1 .data b: .quad 1 z: .quad b-a
z: % gcc -c -o z.o z.s % otool -rv z.o Relocation information (__DATA,__data) 2 entries address pcrel length extern type scattered symbolnum/value 00000008 False quad True SUB False a 00000008 False quad True UNSIGND False b
まぁ,だから何,という話ですが..
R_386_8
ELF/i386な再配置情報のR_386_8とかいったいいつ使うんだろうかと思ってたけど次のようにすれば現れることに気づいた
例) a.s
.data a: .byte 1 d: .byte a
普通にコンパイルするとベースアドレスが0x8048000なので.byte領域に収まらなくてエラーになるので次のようにしてみるとできる
$ as -o a.o a.s $ objdump -r a.o u.o: file format elf32-i386 RELOCATION RECORDS FOR [.data]: OFFSET TYPE VALUE 00000001 R_386_8 .data $ objdump -D a.o Disassembly of section .data: 00000000 <a>: 0: 01 00 add %eax,(%eax) 00000001 <b>: ...
$ ld -Tdata=0x10 -o a a.o $ objdump -D a Disassembly of section .data: 00000010 <a>: 10: 01 10 add %edx,(%eax) 00000011 <b>: 11: 10 .byte 0x10
まぁだから何,という話..
普通だと使うことなさそう..
ELF/i386 の再配置情報
ELFでi386なときに使われる再配置情報は以下のとおりです(elf.hより).
/* i386 relocs. */ #define R_386_NONE 0 /* No reloc */ #define R_386_32 1 /* Direct 32 bit */ S+A #define R_386_PC32 2 /* PC relative 32 bit */ S+A-P #define R_386_GOT32 3 /* 32 bit GOT entry */ G+A-P #define R_386_PLT32 4 /* 32 bit PLT address */ L+A-P #define R_386_COPY 5 /* Copy symbol at runtime */ #define R_386_GLOB_DAT 6 /* Create GOT entry */ S #define R_386_JMP_SLOT 7 /* Create PLT entry */ S #define R_386_RELATIVE 8 /* Adjust by program base */ B+A #define R_386_GOTOFF 9 /* 32 bit offset to GOT */ S+A-GOT #define R_386_GOTPC 10 /* 32 bit PC relative offset to GOT */ GOT+A-P
S: 再配置するシンボルのアドレス
P: 再配置する領域のアドレス
GOT: GOTのアドレス
B: オブジェクトファイルがロードされた位置
L: PLTのエントリのアドレス
A: addend
ほんとはelf.hには8bitに対する再配置のR_386_8とかTLS用の再配置情報とかもっとたくさんあるんですがとりあえず基本はこれのようなので上記のそれぞれついて実際どのような場合に生じるのか調べてみます(SystemVのx86のABI(www.sco.com/developers/devspecs/abi386-4.pdf)では上の11個の再配置情報が定義されています.bfd/elf32-i386を見るとGNU elf extensionsと書いてあるのでどうやら拡張らしいですけどよく詳しいことわかってません.sunで使われてる?).
この辺りの話は環境依存ですが実行環境はUbuntu10.4LTS(linux 2.6.32),gcc 4.4.3です.
参考:
・http://www.amazon.co.jp/%E3%83%AA%E3%83%B3%E3%82%AB%E3%83%BB%E3%83%AD%E3%83%BC%E3%83%80%E5%AE%9F%E8%B7%B5%E9%96%8B%E7%99%BA%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF%E2%80%95%E5%AE%9F%E8%A1%8C%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E5%BF%85%E9%A0%88%E3%81%AE%E6%8A%80%E8%A1%93-COMPUTER-TECHNOLOGY-%E5%9D%82%E4%BA%95-%E5%BC%98%E4%BA%AE/dp/4789838072
・http://netwinder.osuosl.org/users/p/patb/public_html/elf_relocs.html
・http://www.acsu.buffalo.edu/~charngda/elf.html
・http://wiki.osdev.info/?ELF%2F%BC%C2%B9%D4%BB%FE%A4%CE%CF%C3%2F%A5%D7%A5%ED%A5%BB%A5%C3%A5%B5%A4%CB%B0%CD%C2%B8%A4%B9%A4%EB%CF%C3%2Fi386
・const char* const p = "ABC"; と const char q[] = "ABC"; はどちらがよいか、みたいな与太 - memologue
R_386_32 / R_386_PC
通常の再配置です.R_386_32は32bitの絶対値,R_386_PCは32bitのPC相対値です.これらは通常 -c フラグ付きでコンパイルしたオブジェクトファイルの断片内に存在します.具体的にはR_386_32は大域変数や文字列定数を参照する場面,R_386_PCは関数を呼び出す場面等で利用されます.
例:a.c
int a; int f(){ return; } int main(){ a = 1; f(); }
$ gcc -c -o a.o a.c $ objdump -r -d a.o a.o: file format elf32-i386 Disassembly of section .text: 00000000 <f>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 5d pop %ebp 4: c3 ret 00000005 <main>: 5: 55 push %ebp 6: 89 e5 mov %esp,%ebp 8: c7 05 00 00 00 00 01 movl $0x1,0x0 f: 00 00 00 a: R_386_32 a 12: e8 fc ff ff ff call 13 <main+0xe> 13: R_386_PC32 f 17: 5d pop %ebp 18: c3 ret
0xfの位置で大域変数aに1を代入していますが大域変数aは(ローカル変数のようにスタックではなく).bssセクションに配置されるためまだアドレスが決まっていません.そのため再配置が必要になります.また,0x12の位置で関数fを呼んでいますがcall命令はcall命令の次の命令からの相対アドレスを要求するためのPC相対な再配置が必要になります.ちなみに,call命令の引数とし既に0xfffffffc(-4)が代入されていいますが,これは次の命令からの相対アドレスにするためのオフセットです(リンカは0x13の位置から関数fの相対アドレスを求め,それに-4を加えた値が格納されます).
これらの再配置情報はリンクをおこない実行ファイルを生成するときに解決されます.
$ gcc -o a a.o $ objdump -r a a: file format elf32-i386 # 再配置情報がないので何も表示されない $ objdump -d -r a ... 080483b4 <f>: 80483b4: 55 push %ebp 80483b5: 89 e5 mov %esp,%ebp 80483b7: 5d pop %ebp 80483b8: c3 ret 080483b9 <main>: 80483b9: 55 push %ebp 80483ba: 89 e5 mov %esp,%ebp 80483bc: c7 05 18 a0 04 08 01 movl $0x1,0x804a018 80483c3: 00 00 00 80483c6: e8 e9 ff ff ff call 80483b4 <f> # 80483cb+ffffffe9(-23) = 80383b4 80483cb: 5d pop %ebp 80483cc: c3 ret ...
i386では共有ライブラリを使用しない場合使用する再配置情報はこの2つになります.
だいぶシンプルですね.
ところでこの場合f()はmain()と同じ場所で定義されてるのでリンク前に相対アドレスなら既に判明してるのでは..と思うけどどうなんでしょう.
実際関数f()をstaticで定義するとR_386_PC382は生成されません.
$ cat a.c int a; static int f(){ return; } int main(){ a = 1; f(); } $ gcc -c -o a.o a.c $ objdump -d -r a.o a.o: file format elf32-i386 Disassembly of section .text: 00000000 <f>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 5d pop %ebp 4: c3 ret 00000005 <main>: 5: 55 push %ebp 6: 89 e5 mov %esp,%ebp 8: c7 05 00 00 00 00 01 movl $0x1,0x0 f: 00 00 00 a: R_386_32 a 12: e8 e9 ff ff ff call 0 <f> 17: 5d pop %ebp 18: c3 ret
staticをつけるとリンク時に関数の相対位置が変更されないのが保証されるということ?
以下の話は共有ライブラリを使用する場合の話になります.
R_386_GOT32 / R_386_GOTPC / R_386_GOTOFF / R_386_PLT32
これら4つの再配置情報は共有ライブラリになるもののオブジェクトファイルの断片内に存在します.もう少し分かりやすく言えば gcc -c -fPIC でコンパイルしたときのオブジェクトファイル内に存在します(まぁ実際にはPICにしなくても共有ライブラリは作成できるわけですけどそうすると共有ライブラリロード時にテキスト領域の再配置が必要となって共有ライブラリという名前なのに共有できないみたいな自体が発生するので普通はしないはずです).これらの再配置情報を理解するためにはGOTとPLTについて理解する必要があります.
共有ライブラリは仮想記憶を利用してライブラリのテキスト領域(コード領域)を共有する仕組みですが,共有ライブラリはそれを利用するプログラムのどのアドレスに割り当てられるのかがプログラムを実行時でないと決まりません.そのため,共有ライブラリ内のテキスト領域では絶対アドレスを使用することができません.そこで使われるのがGOTとPLTです.GOTとPLTはそれぞれ共有ライブラリ内の変数および関数を参照する場合に使われます.変数にアクセスする場合にはまずGOTのその変数に対応する箇所にアクセスします.その場所に目的の変数のアドレスが入っています.それを間接参照することで変数cにアクセスします.このGOTが格納する変数のアドレスはライブラリがロードされるときに決定します(後述のR_GLOB_DAT参照).
例を見た方が分かりやすいと思うのでまずR_386_GOT32およびR_386_GOTPCを使用するプログラムを見てみます.
b.c
int c; int g(){ return c; }
$ gcc -o b.o -fPIC b.c $ objdump -d -r b.o b.o: file format elf32-i386 Disassembly of section .text: 00000000 <g>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: e8 fc ff ff ff call 4 <g+0x4> 4: R_386_PC32 __i686.get_pc_thunk.cx // call __i686.get_pc_thunk.cx 8: 81 c1 02 00 00 00 add $0x2,%ecx a: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ // get GOT address e: 8b 81 00 00 00 00 mov 0x0(%ecx),%eax 10: R_386_GOT32 c // access c via GOT 14: 8b 00 mov (%eax),%eax 16: 5d pop %ebp 17: c3 ret Disassembly of section .text.__i686.get_pc_thunk.cx: 00000000 <__i686.get_pc_thunk.cx>: // get pc 0: 8b 0c 24 mov (%esp),%ecx // equavalant to movl %eip,%ecx 3: c3 ret
-fPICオプションをつけてコンパイルすることでgccに位置独立コードを生成するようにさせます.これによりリンクして共有ライブラリを生成したときにテキスト領域に関しては再配置がいらないオブジェクトファイルが生成されます.
さて,共有ライブラリ内で大域変数にアクセスするためにはGOTを経由してアクセスします.まず0x3行目で__i686.get_pc_thunk.cxを呼び出しますがこれはスタックの先頭(関数の戻り先アドレス)を%ecxにいれるだけです.これにより%ecxにcallの次の命令のアドレスが%ecxに入ります.この部分は通常の関数呼び出しなので再配置情報はR_386_PC32です.そして次に%ecxに値を足しますが,ここで足すのは_GLOBAL_OFFSET_TABLE_への相対アドレスです.これにより_GLOBAL_OFFSET_TABLE_の絶対アドレスが%ecxに入ることになります.このようにR_386_GOTPCは_GLOBAL_OFFSET_TABLE_の相対アドレスを求めるために利用されます.そしてさらにそのアドレス(_GLOBAL_OFFSET_TABLE_の先頭)に大域変数cのエントリへのオフセットを加えます.このために使われるのがR_386_GOT32です.これにより%ecxには大域変数cに対応する_GLOBAL_OFFSET_TABLE_の絶対アドレスが入ったことになります.そして,このアドレスの位置に実際の変数cのアドレスが入っているのでそれを間接参照することでcの値を取り出します(14行目).
_GLOBAL_OFFSET_TABLE_はリンク時に作成されるので,このときにこれらの再配置情報を解決します.逆にいえばR_386_GOT32はリンカに対してGOTを作れと指示するようなものなんだと思います.このR_386_GOT32からリンク時に後述のR_386_GLOB_DATが生成されるようです.
▼ここだと_GLOBAL_OFFSET_TABLE_の値は.gotセクションの中央と書いてありますが自分の環境だと_GLOBAL_OFFSET_TABLEの値は.got.pltセクションの先頭でした.
.gotセクションの先頭ではないです(変数に対応するエントリへのアクセスは負のオフセット,PLTに対応するエントリへのアクセスは正のオフセットで指定しているということ.gotセクション先頭を指すよりこっちのが分かりやすいから?)
▼R_386_GOT32は資料だとG+A-PとなっていますがこれってG+Aなんじゃ…?
▼もしもGOTの対応するエントリからの相対アドレスを計算する再配置(GOT+G+A-P)があれば一つ命令の数を減らせるような..GOTを経由するアクセスが連続しておこなわれるときはこうして一つのレジスタにGOTのアドレスいれとけばアクセスしやすいということ?後述のR_386_GOTOFFと同じような処理にするためにこうしてる?
次はR_386_PLT32です.これは共有ライブラリ内で,共有ライブラリの他の関数を呼ぶときに使われます.
(staticな関数を呼ぶときは通常の相対アドレスでの呼び出しになります)
例:c.c
int l(){ return 0; } int g(){ return l(); }
$ gcc -c -fPIC -o c.o c.c $ objdump -r -d c.o c.o: file format elf32-i386 Disassembly of section .text: 00000000 <l>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: b8 00 00 00 00 mov $0x0,%eax 8: 5d pop %ebp 9: c3 ret 0000000a <g>: a: 55 push %ebp b: 89 e5 mov %esp,%ebp d: 53 push %ebx e: 83 ec 04 sub $0x4,%esp 11: e8 fc ff ff ff call 12 <g+0x8> 12: R_386_PC32 __i686.get_pc_thunk.bx 16: 81 c3 02 00 00 00 add $0x2,%ebx 18: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ 1c: e8 fc ff ff ff call 1d <g+0x13> 1d: R_386_PLT32 l // call l via plt 21: 83 c4 04 add $0x4,%esp 24: 5b pop %ebx 25: 5d pop %ebp 26: c3 ret Disassembly of section .text.__i686.get_pc_thunk.bx: 00000000 <__i686.get_pc_thunk.bx>: 0: 8b 1c 24 mov (%esp),%ebx 3: c3 ret
16行目で_GLOBAL_OFFSET_TABLE_の絶対アドレスを取得しているのは先ほどと同じです.ここで_GLOBAL_OFFSET_TABLE_のアドレスを求めているのはこの次に呼ぶPLTの関数でその値を使用するためです.1c行目が関数l()の呼び出しです.ここで,R_386_PLT32となっているのでl()を直接呼び出すのではなく
PLT経由で呼び出しています(PLT自体はリンク時に生成され,R_386_PLT32の位置にその生成したPLTへの相対アドレスが格納されます).これは後述する遅延バインディングをおこなうためです.
実際これをコンパイルすると関数gは以下のようになります.
00000494 <g>: 494: 55 push %ebp 495: 89 e5 mov %esp,%ebp 497: 53 push %ebx 498: 83 ec 04 sub $0x4,%esp 49b: e8 d7 ff ff ff call 477 <__i686.get_pc_thunk.bx> 4a0: 81 c3 54 1b 00 00 add $0x1b54,%ebx 4a6: e8 f1 fe ff ff call 39c <l@plt> 4ab: 83 c4 04 add $0x4,%esp 4ae: 5b pop %ebx 4af: 5d pop %ebp 4b0: c3 ret
ここでl()を直接呼び出すかわりにlの対応するPLTを呼び出します.PLTの内容は次のようになっています.
0000039c <l@plt>: 39c: ff a3 10 00 00 00 jmp *0x10(%ebx) 3a2: 68 08 00 00 00 push $0x8 3a7: e9 d0 ff ff ff jmp 37c <_init+0x30>
%ebxには直前の操作によって_GLOBAL_OFFSET_TABLE_の値が入っています.つまりこの最初の部分では
GOTのlに対応するエントリにジャンプするわけです.さて,_GLOBAL_OFFSET_TABLE_の値は
計算してみると0x1b54+0x4a0=0x1ff4で,objdumpで見ると次のようになっています.
00001ff4 <.got.plt>: 1ff4: 1c 1f sbb $0x1f,%al ... 1ffe: 00 00 add %al,(%eax) 2000: 92 xchg %eax,%edx 2001: 03 00 add (%eax),%eax 2003: 00 a2 03 00 00 b2 add %ah,-0x4dfffffd(%edx) 2009: 03 00 add (%eax),%eax
l@pltからのジャンプ先は0x10(%ebx)なので,0x1ff4+0x10=0x2004番地目に格納されているアドレスということになります.よく見るとこの値は0x03a2となっていて,つまりl@pltの2行目です.なんでこんな意味の分からないことをしているかというとこれは共有ライブラリの関数の遅延バインディングをおこなうためで,この後l@pltの3行目で呼ばれる関数によって先ほどのGOTの対応する箇所(0x2004番地)に関数l()の番地が書き込まれ,その上で関数l()が呼ばれます.これによって2回目以降の呼び出しはGOTを参照すればすぐに呼べるようになるわけです.多くのライブラリ関数を使うようになると初めのロード時に全ての呼び出し番地を解決していたら時間がかかるためこのように必要になってときのみアドレス解決をおこなうようになっています.ただし,遅延バインディングといってもGOTのエントリの初期値はPLTの絶対アドレスを持つ必要があるため,共有ライブラリロード時にGOTのエントリに対しライブラリがロードされたアドレスが加算されます(この場合何か他のシンボルのアドレスを参照したりするわけではなくただベースアドレスを加算するだけなので高速に処理できるようです).
ちなみに,共有ライブラリではなく共有ライブラリを利用する側のプログラムではpltは少し違います.
例えばこのプログラム
hello.c
#include <stdio.h> int main(){ printf("hello\n"); }
をコンパイルしてみます.
$ gcc -o hello.o -c hello.c $ objdump -rd hello.o hello.o: file format elf32-i386 Disassembly of section .text: 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 e4 f0 and $0xfffffff0,%esp 6: 83 ec 10 sub $0x10,%esp 9: c7 04 24 00 00 00 00 movl $0x0,(%esp) c: R_386_32 .rodata 10: e8 fc ff ff ff call 11 <main+0x11> 11: R_386_PC32 puts 15: c9 leave 16: c3 ret
この状態ではprinf()は共有ライブラリ関数なのかどうかも分からない未解決の状態です
これをリンクすると,次のようになります.
$ gcc -o hello hello.o $ objdump -D hello 08048318 <puts@plt>: 8048318: ff 25 08 a0 04 08 jmp *0x804a008 804831e: 68 10 00 00 00 push $0x10 8048323: e9 c0 ff ff ff jmp 80482e8 <_init+0x30> ... 080483e4 <main>: 80483e4: 55 push %ebp 80483e5: 89 e5 mov %esp,%ebp 80483e7: 83 e4 f0 and $0xfffffff0,%esp 80483ea: 83 ec 10 sub $0x10,%esp 80483ed: c7 04 24 c0 84 04 08 movl $0x80484c0,(%esp) 80483f4: e8 1f ff ff ff call 8048318 <puts@plt> 80483f9: c9 leave 80483fa: c3 ret ... 08049ff4 <_GLOBAL_OFFSET_TABLE_>: 8049ff4: 20 9f 04 08 00 00 and %bl,0x804(%edi) 8049ffa: 00 00 add %al,(%eax) 8049ffc: 00 00 add %al,(%eax) 8049ffe: 00 00 add %al,(%eax) 804a000: fe 82 04 08 0e 83 incb -0x7cf1f7fc(%edx) 804a006: 04 08 add $0x8,%al 804a008: 1e push %ds 804a009: 83 .byte 0x83 804a00a: 04 08 add $0x8,%al ...
最適化によってprintfがputsになっていますが,mainの804834のところでputs@pltが呼ばれています.
puts@pltでは共有ライブラリの場合異なり,絶対アドレスで_GLOBAL_OFFSET_TABLE_内のエントリにジャンプしています.実行形式の場合ロードされるアドレスは既にこの時点で確定しているので共有ライブラリのようにわざわざ相対的にアクセスする必要がないわけです.逆にいうとR_386_PLT32の役割というのはリンカに対して_GLOBAL_OFFSET_TABLE_内の対応するエントリに相対的にジャンプするようなPLTのコードを生成せよと伝える役目があるのだと思います(たぶん
というか最初の例は-fPICでコンパイルしてるからPLTもPICになってると解釈する方がいいのかも.
▼わざわざPLTでGOTを経由することなく直接PLT内を再配置した方が間接ジャンプする必要も余分なGOTエントリを作る必要もなく,実行時のコストもプログラムの容量的にも優れてるんじゃ..と思いますがこれはプログラムコードであるPLTを実行可能かつ読み込み可能としてロードするためだと思います.また,こう
しておけばPLTも複数のプログラムから共有できるようになります.PLTを直接再配置するアーキテクチャもあるようです.
次はR_386_GOTOFFです.
これは共有ライブラリ内で文字列定数やstatic変数を参照するときに使います.文字列定数やstatic変数はライブラリ内で使用するもので外部に公開するわけではないのでGOTを経由する必要はありませんがどこにロードされるのか分からないためシンボルを参照するときは再配置が必要になります.
d.c
static int c = 3; int g(){ return c; }
$ gcc -c -fPIC -o d.o d.c $ objdump -d -r d.o d.o: file format elf32-i386 Disassembly of section .text: 00000000 <g>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: e8 fc ff ff ff call 4 <g+0x4> 4: R_386_PC32 __i686.get_pc_thunk.cx 8: 81 c1 02 00 00 00 add $0x2,%ecx a: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ e: 8b 81 00 00 00 00 mov 0x0(%ecx),%eax 10: R_386_GOTOFF .data 14: 5d pop %ebp 15: c3 ret Disassembly of section .text.__i686.get_pc_thunk.cx: 00000000 <__i686.get_pc_thunk.cx>: 0: 8b 0c 24 mov (%esp),%ecx 3: c3 ret
一見R_386_GOT32の例と似ていますが,R_386_GOT32と違い間接参照せず直接変数cの値を求めています.R_386_GOTOFFにはシンボルのGOTからのオフセットが入っているので上の例のようにすれば変数にアクセスできます.(0xeの位置ので%eaxにcの値が入る.この例だとcをreturnにしてるだけなので%eaxに入れたら終わり).
▼これって自分の位置からデータまでの相対アドレスじゃだめなの..?
R_386_JMP_SLOT
これは共有ライブラリの関数を呼び出す実行ファイル内に存在します.共有ライブラリが他の共有ライブラリの関数を呼ぶ場合にはその共有ライブラリ内にも存在します(その場合はR_386_PLT32によって作られたPLTと対応します).
例えばある共有ライブラリ(ここではlibhoge.soとする)を利用する次の関数を考えます.
e.c
int main(){ f(); // 共有ライブラリの関数fを呼び出す return 0; }
これを次のようにコンパイルすると,
$ gcc -o e -lhoge
再配置情報は,
e: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE ... 0804a008 R_386_JUMP_SLOT f
こんな感じで具体的に0x0804a008の箇所を見てみると
$ objdump -D e ... Disassembly of section .got.plt: 08049ff4 <_GLOBAL_OFFSET_TABLE_>: 8049ff4: 18 9f 04 08 00 00 sbb %bl,0x804(%edi) 8049ffa: 00 00 add %al,(%eax) 8049ffc: 00 00 add %al,(%eax) 8049ffe: 00 00 add %al,(%eax) 804a000: de 83 04 08 ee 83 fiadd -0x7c11f7fc(%ebx) 804a006: 04 08 add $0x8,%al 804a008: fe .byte 0xfe 804a009: 83 .byte 0x83 804a00a: 04 08 add $0x8,%al ...
と,_GLOBAL_OFFSET_TABLE_の一部をさしています.つまりこの位置が関数fに対応するGOTのエントリというわけです.実際この初期値は0x080483feで,この位置をobjdumpで調べてみると,
080483f8 <f@plt>: 80483f8: ff 25 08 a0 04 08 jmp *0x804a008 80483fe: 68 10 00 00 00 push $0x10 8048403: e9 c0 ff ff ff jmp 80483c8 <_init+0x30>
と,関数fに対応するPLT内を指しています.関数f()の1回目の呼び出しはこのPLTを経由しておこなわれ,このときにこのR_386_JUMP_SLOTに対応するアドレスが書き変わるので2回目以降はPLTを経由することなしにf()が呼ばれるというわけです.本当にこうなってるのかはgdbで簡単に確認できます(関数呼び出し前と後で対応するGOTのエントリを見るだけ).
R_386_GLOB_DAT / R_368_COPY
この2つは対のようなもので,R_386_GLOB_DATは共有ライブラリが,ライブラリ内の大域変数を参照するために用いられ,R_386_COPYはライブラリを利用するプログラムがライブラリ内の大域変数を使用するために用いられます.
foo.c
int a = 3; int f(){ return a; }
bar.c
extern int a; int main(){ a += 1; return 0; }
こんなそれぞれこんな感じにコンパイルします
$ gcc -fPIC -shared -o libfoo.so.1 foo.c $ gcc -fPIC -shared -o libfoo.so.1 -Wl,-soname=libfoo.so.1 foo.c $ ln -s libfoo.so.1 libfoo.so $ gcc -L ./ -lfoo -o bar bar.c
ここでまずbarの再配置情報を見てみます.
$ objdump -R bar
bar: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ff0 R_386_GLOB_DAT __gmon_start__
0804a010 R_386_COPY a
0804a000 R_386_JUMP_SLOT __gmon_start__
0804a004 R_386_JUMP_SLOT __libc_start_main
変数aに対する再配置情報がR_386_COPYとなっています.対応する位置0x804a010はどこかというと,これは実は.bssセクションになります.
$ readelf -S bar There are 30 section headers, starting at offset 0x1124: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ... [25] .bss NOBITS 0804a010 001010 00000c 00 WA 0 0 4 ...
barは実行時にこの.bssセクションにaの値をコピーします.そしてそれ以降はそのコピーしたaに対して操作をおこなうわけです.この時点ではまだbarにはaの実態がないので初期化しない大域変数と同じような扱いをするわけです.ということで逆にライブラリ関数側でaを操作する場合にはbarの.bssセクション内の変数を操作する必要があります.これを可能にするのがR_386_GLOB_DATです.libfoo.soの再配置情報を見てみると(ここまできてreadelf使った方が情報多いことに気づいた..),
$ readelf -r libfoo.so Relocation section '.rel.dyn' at offset 0x2fc contains 5 entries: Offset Info Type Sym.Value Sym. Name 00002008 00000008 R_386_RELATIVE 00001fe4 00000106 R_386_GLOB_DAT 00000000 __gmon_start__ 00001fe8 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses 00001fec 00000706 R_386_GLOB_DAT 0000200c a 00001ff0 00000306 R_386_GLOB_DAT 00000000 __cxa_finalize Relocation section '.rel.plt' at offset 0x324 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00002000 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__ 00002004 00000307 R_386_JUMP_SLOT 00000000 __cxa_finalize
fの中身を見てみると,
0000045c <f>: 45c: 55 push %ebp 45d: 89 e5 mov %esp,%ebp 45f: e8 10 00 00 00 call 474 <__i686.get_pc_thunk.cx> 464: 81 c1 90 1b 00 00 add $0x1b90,%ecx 46a: 8b 81 f8 ff ff ff mov -0x8(%ecx),%eax 470: 8b 00 mov (%eax),%eax 472: 5d pop %ebp 473: c3 ret
45fから46aでGOTのエントリにアクセスしています.そのアドレスは0x464+0x1b90-0x8=1efcとなっていてさっき調べた変数aに対応するR_386_GLOB_DATに対応しています.この場所をライブラリロード時に
書き換えることによってbarの.bss領域のaにアクセスするわけです.ちなみに変数に対しては遅延バインディングのようなものはありません.コピーされるaのデータは?というとこれはちゃんとlibfoo.soの.dataセクションに入っています.上のreadelfのaのところを見るとSym.Valueが200cになっていますがこれがaの値が格納されている箇所のアドレスです.
Disassembly of section .data: ... 0000200c <a>: 200c: 03 00 add (%eax),%eax ...
と,ここまで書いて本当にそうなってるのか疑問になったのでgdbで確かめてみます.(実行する際には共有ライブラリを使用するので export LD_LIBRARY_PATH="./" などとする必要があります)
$ gdb bar ... (gdb) b main Breakpoint 1 at 0x80484b7 (gdb) r Starting program: /home/m/tmp/bar Breakpoint 1, 0x080484b7 in main () (gdb) maintenance info sections Exec file: `/home/m/tmp/bar', file type elf32-i386. ... 0x804a010->0x804a01c at 0x00001010: .bss ALLOC ...
maintenanc info sectionsとするとセクションの配置されているアドレスが分かります..bssのアドレス
は当然先ほど調べたものと同じ0x804a010番地です.この番地を調べてみると,
(gdb) x/10wx 0x804a010 0x804a010 <a>: 0x00000003 0x00000000 0x00000000 0x00000000 0x804a020: 0x00000000 0x00000000 0x00000000 0x00000000 0x804a030: 0x00000000 0x00000000
と,無事に3がコピーされています.さて,次はlibfoo.soを調べてみます.disassemble fとすればfoo.c内の関数f()が表示されます.
(gdb) disassemble f Dump of assembler code for function f: 0x0012e45c <+0>: push %ebp 0x0012e45d <+1>: mov %esp,%ebp 0x0012e45f <+3>: call 0x12e474 <__i686.get_pc_thunk.cx> 0x0012e464 <+8>: add $0x1b90,%ecx 0x0012e46a <+14>: mov -0x8(%ecx),%eax 0x0012e470 <+20>: mov (%eax),%eax 0x0012e472 <+22>: pop %ebp 0x0012e473 <+23>: ret End of assembler dump.
ここで0x0012e470のところで変数aにアクセスしているので,このときの%eaxの値が0x804a010ならばいいわけです.%eaxに入っている値が格納されている番地は,0x0012e464+0x1b90-0x8 = 0x12ffcなので,この番地を見てみます.
(gdb) p/x 0x0012e464+0x1b90-0x8 $1 = 0x12ffec (gdb) x/10wx 0x0012e464+0x1b90-0x8 0x12ffec: 0x0804a010 0x00160500 0x00001f14 0xb7fff8a8 0x12fffc: 0x00123270 0x0012e37a 0x0012e38a 0x00130008 0x13000c <a>: 0x00000003 0x00000000
ちゃんと0x12ffcには0x0804a010が入ってました.
共有ライブラリで完全に共有されるのはテキスト領域のみで,static変数おかれたりする.data領域等はコピーオンライトでプロセスごとに作成されます.この部分も共有ライブラリロード時にそういう風な処理になってるんだと思います
R_386_RELATIVE
これは共有ライブラリとしてリンクが終わったオブジェクトファイル内に存在し,共有ライブラリ内でシンボルのアドレスを参照するときなどに使われます(正確にいうと-sharedをつけてコンパイルしたものの中.PICかどうかは関係ない.たぶん).具体的にいうと共有ライブラリ内でポインタが文字列定数のアドレスを指す場合などです.
例:e.c
#include <stdio.h> char *hello = "hello"; void f(){ printf("%s\n",hello); }
e.o: file format elf32-i386 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 00000008 R_386_PC32 __i686.get_pc_thunk.bx 0000000e R_386_GOTPC _GLOBAL_OFFSET_TABLE_ 00000014 R_386_GOT32 hello 0000001e R_386_PLT32 puts RELOCATION RECORDS FOR [.data.rel.local]: OFFSET TYPE VALUE 00000000 R_386_32 .rodata
コンパイルしただけでは再配置はR_386_32ですが,これを-sharedでリンクして共有ライブラリを作ると,
$ gcc -shared -o libe.so.1 -Wl,-soname=libe.so.1 e.o $ objdump -R libe.so.1 e: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 0000200c R_386_RELATIVE *ABS* 00002010 R_386_RELATIVE *ABS* 00001fe4 R_386_GLOB_DAT __gmon_start__ 00001fe8 R_386_GLOB_DAT _Jv_RegisterClasses 00001fec R_386_GLOB_DAT hello 00001ff0 R_386_GLOB_DAT __cxa_finalize 00002000 R_386_JUMP_SLOT __gmon_start__ 00002004 R_386_JUMP_SLOT puts 00002008 R_386_JUMP_SLOT __cxa_finalize
このようにR_386_RElATIVEが生成されています.__gmon_start__とかはリンク時にくっつもので,これはプロファイラなどに使われるようですがここでは無視します.ここで重要なのは上から2番のR_386_RELATIVEです.OFFSETが2010となっていますが,これは実は.dataセクションを指しており,この中に"hello"を指すアドレスが入っています.
具体的に確かめてみると,
$ readelf -S libe.so.1 There are 28 section headers, starting at offset 0x1114: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al .... [22] .data PROGBITS 0000200c 00100c 000008 00 WA 0 0 4 ...
このように0x200cから.dataセクションです.
objdumpでも確認してみると,
$ objdump -D libe.so.1 Disassembly of section .data: ... 00002010 <hello>: 2010: 24 05 and $0x5,%al 2010: R_386_RELATIVE *ABS* ...
と,初期値として0x0524が入っています(リトルエンディアン).同じくobjdumpの出力で
0x524番地を見てみると,
Disassembly of section .rodata: 00000524 <.rodata>: 524: 68 65 6c 6c 6f push $0x6f6c6c65 # 文字列hello
となっています.ということで変数helloの値はとりあえず0x0524なわけですが共有ライブラリロード時に
この値にライブラリがロードされた位置を足すことで実際のアドレスになるというわけです.ちなみに,ポインタではなく文字列をそのまま引数にした場合(つまり,printf("%s\n","hello!");とした場合)は再配置情報は通常ならR_386_32,PICならR_386_GOTOFFとなり.rodata内の文字列を直接指すようになります.char hello[] = "hello" のように配列にした場合も同様で直接.rodataを指します.さらに補足すると,グローバルな変数や関数のアドレスを参照する場合は普通にR_386_32です.
例:
f.c
int x = 3; int *px = &x;
これを次のようにコンパイルしてreadelfしてみると,
$ gcc -shared -fPIC -o f f.c $ redelf -r v Relocation section '.rel.dyn' at offset 0x2f0 contains 5 entries: Offset Info Type Sym.Value Sym. Name ... 00002010 00000401 R_386_32 0000200c x ...
となります..dataセクションを見ると,
Disassembly of section .data: ... 0000200c <x>: 200c: 03 00 add (%eax),%eax ... 00002010 <px>: 2010: 00 00 add %al,(%eax) ...
となっていて,0x2010番地のpxに0x200c番地のxのアドレスを格納することを指示する再配置情報になっています.
ということで長々と再配置について調べてきましたがこうしてみてみると共有ライブラリまわりのところは
本当にうまくできてるなと思います.まだちょっと理解の足らないとこがありますがのでこれ以上はソースを読まないと..
▼Linkers and Loadersもじっくり読んでみるかな-
Vimでお絵描きプログラミング
この記事はVim Advent Calender 2012,第23日目の記事です.前日は@y_uukiさんのPerl屋さんに便利なVim Pluginを2つ書いた - ゆううきブログでした.
pietというプログラミング言語があります.pietは普通の言語とはちょっと変わっていて画像を使ってプログラミングをおこないます.例えば"Hello World"を出力するプログラムの1つは次のようになります(DM's Esoteric Programming Languages - Piet Samplesより)
さて,このpietのプログラムを作成するにはペイントとかいうエディタを使ったりするらしいですがプログラムを書くのはvimの本分ですからね.vimで書けない訳ないですよね.ということで作りました.
GitHub - mmisono/piet.vim: vim plugin for piet
pietについて
pietについては公式かwikipedia(日本語)を参考にしてください.
pietのプログラムを普通に実行したいときにはnpietがおすすめです.npietはppm,png,gifに対応しています.
pietは簡単にいってしまうといわゆるスタックマシンです.どの命令を実行するのかということを色の明度(lightness)と色相(hue)の違いによって決定します.同じ色でつながっている領域のセルの数がpushしたときにスタックに積まれる値となります.本来ならば1ピクセルがそれぞれ意味を持ちますが,1ピクセルだとプログラミングをするときに不便なので,実際は10x10などを一まとまりとして扱います.これをpietではcodelと呼んでいます.
次に移動する場所を決定するDPとCCの動きが少し分かりにくいと思うので以下に少し説明します.DPが取りうる値は"right","down","light","up"の4つ,CCが取り売る値は"left","right"の2つです.初期状態ではDP="right",CC="left",初期位置は左上です.プログラムが実行されると,現在の位置からまずDPの方向に最も遠いcodelを探します.もし,DPの方向に最も遠いcodelが複数あるならば,自分がDPの方向に向かってるとして,その向きからCCの方向に最も遠いcodelが選択されます,そして,その選択したcodelからDP側にあるcodelが次に移動する位置となり,そのcodelとの色の違いで実行する命令が決定されます.もしも移動先のcodelが黒or端ならば,まずCCがトグルされ,再び次の移動先を探します.もしもまた移動先のcodelが黒or端ならば,今度はDPを時計回りに変えます.このように,移動先が黒or端ならばCC->DP->CC->DP...のようにCCやDPの値を変えて次の移動先を探します.もし8回試しても見つからないようならばどこにも移動することがないということなのでプログラムは終了します.
仮に移動先codelが白だった場合は,そのままDPの方向に白以外のcodelにぶつかるまで進みます.この動作は通常の移動先を決定する動作とは違いCCは考慮されません.もし白以外のcodelが見つかればそこに移動します(何も命令は実行しません).もしこの時黒のcodelや端にぶつかってしまったときはDPを時計周りに変化させかつCCをトグルします.そして再びDPの方向に直進し,移動先を探します.これを移動先が見つかるまでおこないます.もしも移動先が見つからずループするようならばプログラムの実行は終了します.ただし,npietのデフォルトの動作は白から黒や端に遷移したときはぶつかる手前の位置に移動し,CCとDPを変化させるようになっています.npietでこの動作をするよう帰るには"-v11"オプションを指定しますpiet.vimではデフォルトでここで説明した動作です.もしnpietのデフォルトと
同じ動作をさせるためには"-stay"オプションをつけます.
piet.vimについて
piet.vimはPPM(P3)にのみ対応しています.
以下のコマンドがあります.
・ReadFromPPM
PPM(P3)のファイルからpiet.vimが扱える形式に変換して読み込みます.ちなみにpngやgifからPPM(P3)の画像に変換するにはimage magicを使って以下のようにできます.
$ convert -compress none foo.png foo.ppm
・PietEdit
pietの編集を開始します.ReadFromPPMを実行するとこのコマンドは自動的に実行されます.piet.vimでは2文字が1codelになります.このコマンドを実行すると以下のマッピングが定義されます.
ノーマルモード
・ H : カーソル下を現在の色に変更し左へ移動
・ J : カーソル下を現在の色に変更し下へ移動
・ K : カーソル下を現在の色に変更し右へ移動
・ L : カーソル下を現在の色に変更し右へ移動
・ U : 現在の色をpushに対応する色へ変更
・ OO : 現在の色をpopに対応する色へ変更
・ + : 現在の色をaddに対応する色へ変更
・ - : 現在の色をsubtractに対応する色へ変更
・ * : 現在の色をmultiplyに対応する色へ変更
・ / : 現在の色をdivideに対応する色へ変更
・ % : 現在の色をmodに対応する色へ変更
・ N : 現在の色をnotに対応する色へ変更
・ > : 現在の色をgreaterに対応する色へ変更
・ P : 現在の色をpointerに対応する色へ変更
・ S : 現在の色をswitchに対応する色へ変更
・ D : 現在の色をduplicateに対応する色へ変更
・ R : 現在の色をrollに対応する色へ変更
・ IN : 現在の色をin(number)に対応する色へ変更
・ IC : 現在の色をin(char)に対応する色へ変更
・ ON : 現在の色をout(number)に対応する色へ変更
・ OC : 現在の色をout(char)に対応する色へ変更
・ w : 現在の色を白へ変更
・ k : 現在の色を黒へ変更
・ <C-c>r : 現在の色を赤へ変更
・ <C-c>y : 現在の色を黄へ変更
・ <C-c>g : 現在の色を緑へ変更
・ <C-c>c : 現在の色をシアンへ変更
・ <C-c>b : 現在の色を青へ変更
・ <C-c>m : 現在の色をマゼンダへ変更
・c : 現在の色をカーソルしたの色に変更
また,<C-c><C-b> とすると明るい青,<C-c>Bとすると暗い青へ現在の色を変更します.他も同様です.
対応する色に変更というのは,例えば現在の色が赤のとき'-'を押すと現在の色が暗い黄色になります(色相を1ステップ,明るさを1つ暗くするのがsubtractなので)
インサートモード
・r : 赤
・y : 黄
・g : 緑
・c : シアン
・b : 青
・m : マゼンダ
また,ノーマルモードで色を変更するのと同様に<C-b>で明るい青,Bで暗い青を入力します.他も同様です.
まぁ,通常であればノーマルモードでの編集で十分だと思います.
このモードになるとエコーラインに以下の表示が出ます.
ここで左側が現在の色,右側がカーソル下の色です.また,移動した時にカーソル下の色が変化した場合にはその変化に対応するコマンドが右端に出ます.
・PietEditOff
pietの編集を終了します.実態はバッファローカルなマッピングやautocmdを消すだけ..
・SaveAsPPM
現在のファイルをPPMとしてエクスポートします.保存する場合のデフォルトのcodelサイズは1x1です.引数で指定することもできます.
・PietRun
現在編集しているpietのファイルを実行します.領域の面積を計算するのに再帰を使っていますが,大きいプログラムだと再帰が深すぎてエラーがでることがあります.
とりあえず
:setl mfd=500
とかすれば一応対処できますがmfdの値を変えても無理なようならnpietを使ってください..
以下適当な補足など
・pietの存在を知った瞬間,直感的にvimならいける,と思い勢いそのままに作ってみましたがあんまり操作性よくないかも(あれ..
・vim advent calender 2012 13日目の記事を書かれた@ne_sachirouさんがその記事の少し前でpietについて書かれていたのでちょっとびっくりしました.時代はpietなんですかね
・ターミナルでも色が対応してればいけます
・インタプリタは碌にテストしてません.バグあるかもしれません
・quickrunがあるならば以下のようにしてnpietで実行できます(やっつけ)
if !exists('g:quickrun_config') let g:quickrun_config = {} endif let g:quickrun_config.piet = \ { \ 'command' : 'npiet', \ 'cmdopt' : '-v11', \ 'exec' : ['%c %o %S:p:r.ppm'], \ 'tmpfile' : '%{tempname()}.ppm', \ 'runner' : 'system', \ 'hook/sweep/files' : '%S:p:r.ppm', \ 'hook/saveasppm' : '' \ } let s:hook = {} let s:hook.kind = "hook" let s:hook.name = "saveasppm" function! s:hook.on_hook_loaded(session,context) exe "SaveAsPPM ".expand("%:p:r").".ppm" endfunction call quickrun#module#register(s:hook)
動作はこちらの方が確実です
・ビジュアルモードちゃんと対応してないですorz (うまくいかない..
・というかソースひどいorz
次はもっと実用的なもの作るようがんばります..
明日は@kozo2さんです.