const char* const p = "ABC"; と const char q[] = "ABC"; はどちらがよいか、みたいな与太
諸事情あって、ふと前に読んだドキュメントに書いてあった細かいことが気になった。いやいつも細かいけど。
const char* const p = "ABC";
より
const char q[] = "ABC";
のほうがいいのか?的な話。後者の方が良いらしいので、確認するととともに、すぐになんでも書くのはどうなんだと思いつつも無駄に細かく解説。いろいろ間違ってたらゴメンナサイ。C言語入門?
先に結論
共有ライブラリやPIEな実行ファイルを作る場合は、後者の書き方(const char q[] = "xxx")のほうが良さそうですね。PIEじゃない単なる実行ファイルを作るときは、最適化かけるならあんまりかわらないかも。
比較1) コンパイル時の最適化の効きやすさ
最適化といってもいろいろありますが、↓に限って言えば、const char q[] のほうが効きやすいようですね。
gcc version 4.1.1 20060525 (linux/x86) で、最適化なしだと、printf("%zu", strlen(p)); はstrlen関数を呼んでしまうけど、printf("%zu", strlen(q)); は第二引数部分が即値0x3にコンパイルされました。でも、-O2すれば両方0x3になるので、あんまり違わないともいえます。なお、"z" はsize_tとssize_tに対応する修飾文字です。どうでもいいトリビア。
比較2) 実行中の時間的なコスト
比較2の結論も先に書くと、「単に実行ファイルを作る場合ならポインタ版と配列版に違いなし。PICかPIEにする場合、間接参照が1段少ない分、配列版のほうが速そうに見えますがどうなんだろ」となります。
static const char* const gsp = "AAA"; const char* const gpp = "BBB"; static const char gsa[] = "CCC"; const char gpa[] = "DDD"; void bar() { printf("static pointer = %s\n", gsp); printf("public pointer = %s\n", gpp); printf("static array = %s\n", gsa); printf("public array = %s\n", gpa); }
のようなコードを準備。確認していきます。
(1) PIC
% gcc -fPIC -shared -o shared.so bar.c % nm shared.so | grep 適当に抜粋 | sort 0000070a r gsa 0000070e R gpa 00001784 d gsp 00001788 D gpp 0000186c a _GLOBAL_OFFSET_TABLE_
PICでコンパイルしてnmしたら上記のようになりました。ポインタ版は.data (か.data.ro.relro)に、配列版は.rodataに置かれたようです。次に、readelfコマンドでセクションの一覧を出しておきます。
% eu-readelf -S shared.so | egrep -e '(got|data|rodata)' [12] .rodata PROGBITS 000006e0 0006e0 00008a 1 AMS 0 0 1 [17] .data.rel.ro PROGBITS 00001784 000784 000010 0 WA 0 0 4 [19] .got PROGBITS 00001854 000854 000018 4 WA 0 0 4 [20] .got.plt PROGBITS 0000186c 00086c 000018 4 WA 0 0 4 [21] .data PROGBITS 00001884 000884 00000c 0 WA 0 0 4
適当に抜粋しました。最後にsoをobjdump(逆アセ)します。
% objdump -d shared.so | less
00000616 <bar>: ... 61d: e8 a5 ff ff ff call 5c7 <__i686.get_pc_thunk.bx>
__i686.get_pc_thunk.bx は、gccが用意してくれる関数で、PC(プログラムカウンタ、EIPレジスタの値)をebxにコピーします。姉妹関数で __i686.get_pc_thunk.cx もあります。こちらはPCをecxにコピーします。caller-saved registerであるecxにコピーのほうが若干コードを短く出来るので、状況*1によっては勝手にそちらが使われます。なんでここでPCが必要かというと、グローバル変数にアクセスしたいからです。PIC/PIEの場合、実行時に自分自身(ELFバイナリ)がどの仮想アドレスにmmapされるかはわかりませんので、アドレス決め打ちで変数にアクセスはできません。でも、いま実行している命令(61d:)から変数までのオフセットは*.oをリンクしてsoにした時点で判明します(soファイルのナカミがほぼそのままメモリに貼られるわけなんで)。
mov 0xほげ(%eip) ふが; とか出来れば一番いいんですが(x86_64はできる)、x86はそういうPC相対のアドレス指定はできません。というか、eipを直接的に得ることすら出来ません。なんで、仕方なく一度call命令を発行して、リターンアドレス(callを発行した命令の次の命令のアドレス)をスタック上に自動pushさせて、__i686.get_pc_thunk.bx内でebxレジスタにpopしてます。x86だと、PIC/PIEなコードでグローバル変数を触るだけで、関数呼び出しと同じようなコストがかかるんざますよ!奥様。なお、gcc3では確か、__i686.get_pc_thunk.bxみたいな関数はなくって、安直に次の命令に向かって相対なcallをしてたと思います。call いっこ先; popl %ebx; のように。なんで変化したのかは知りません。コードサイズ? 関数をcallという方式にしておけば、毎回popの分、1バイトづつケチれそうではありますね。さて次。
622: 81 c3 4a 12 00 00 add $0x124a,%ebx
PCの値にリンカが決めた適切なオフセットを足して、_GLOBAL_OFFSET_TABLE_ というシンボルのアドレスにします。なお、addする値は、*.o の時にはわからないのでダミーの値になっています。*.o の .rel.text というセクションに、「あとで適切な値をうめてケロ」と書いてあります。*.o を objdump -dr すると、R_386_ほげ みたいな記載になっているところが、TBDな場所です。*.o を so にまとめるときに適切な値を埋めることを、(リンク時の)リロケーションとか呼びます。
このadd命令の場所と、addで足される値の和をgdbを電卓として使って計算してみると、
(gdb) p/x 0x622+0x124a $1 = 0x186c
で、nmしたときの __GLOBAL_OFFSET_TABLE__ シンボルの値と一致しています。実行時のadd結果は、この0x186cにDSOのロードアドレス(DSOの先頭のアドレス)を足したものになるわけです。これ以降、このebxに格納された _GLOBAL_OFFSET_TABLE_ のアドレスを基準として変数に触ります。このときのebxをPICレジスタとか呼んだりもします。たぶん。以降、面倒なので _G_O_T_ と略記します。GOTだと、.gotセクションの先頭アドレスと紛らわしいので。なんで一致していないのかは良く知りませんが、Sunのマニュアルを見た感じだと、「正および負のオフセットでなるべく多くの範囲を指せる様」にシンボルの位置を後ろにずらしてあるとかないとか。
;; ここから static const char* const gsp = "AAA"; へのアクセス処理 628: 8b 83 18 ff ff ff mov 0xffffff18(%ebx),%eax 62e: 89 44 24 04 mov %eax,0x4(%esp)
0xffffff18は、gdb電卓様によれば -232 です。_G_O_T_ のアドレス 0x186c からこれを引いてみると0x1784です。このあたりは、先のreadelf -S の結果と比べると、.data.rel.ro セクションです。nmの結果と 0x1784 を見比べると、ジャストでgspのアドレスであることがわかります。2番目のmov命令で、gspの内容(="AAA"のアドレス)をスタックに積んでいます。これがprintf関数に第二引数として渡ります。
632: 8d 83 aa ee ff ff lea 0xffffeeaa(%ebx),%eax 638: 89 04 24 mov %eax,(%esp) 63b: e8 84 fe ff ff call 4c4 <printf@plt>
最初の2行は、フォーマット文字列を引数として積んでいるだけ、3行目はprintfを呼んでるだけです。PLT経由の関数呼び出しは、すでに前に書いたので省略します。今後、printfとその前の2行は略します。
;; const char* const gpp = "BBB"; のアクセス 640: 8b 83 e8 ff ff ff mov 0xffffffe8(%ebx),%eax 646: 8b 00 mov (%eax),%eax 648: 89 44 24 04 mov %eax,0x4(%esp)
0xffffffe8 は -24 で、計算すると丁度 .got セクションの先頭あたりです。1行目で、まずGOT経由*2で変数gppのアドレスを得ています。2行目で、gppの指し先のアドレス("BBB")をスタックに積んでいます。間接参照の段数が多く、まわりくどいですね!
何故わざわざGOTを経由するのかですが、これは、LD_PRELOADなどで変数gppをinterpose可能にするためです(よね)。DSOの外に公開されているシンボルは、interpose可能でなくてはなりません*3。gppなるシンボルをもつyokodori.soを作成して、LD_PRELOADしてやると、0xffffffe8(%ebx) はshared.soの変数gppではなく、yokodori.soの変数gppを指すようになります。dynamic linler(ld.so)がそのように仕向けます。
;; static const char gsa[] = "CCC"; のアクセス 65a: 8d 83 9e ee ff ff lea 0xffffee9e(%ebx),%eax 660: 89 44 24 04 mov %eax,0x4(%esp)
staticなのでGOTは経由しません。ということで、一見最初のと似ていますが、全然違います。まず、最初の命令がmovでなくlea*4です。そして、0xffffee9e(%ebx)は、.data.rel.ro ではなく .rodata です(計算して readelf -S と見比べてみましょう)。.rodataには、お目当ての CCC が格納されています。確認します:
% nm shared.so | grep 適当に抜粋 | sort 0000070a r gsa 0000186c a _GLOBAL_OFFSET_TABLE_ % gdb (gdb) p/x 0x186c+0xffffee9e $1 = 0x70a % eu-readelf -S shared.so | grep -A 1 rodata [12] .rodata PROGBITS 000006e0 0006e0 00008a 1 AMS 0 0 1 [13] .eh_frame PROGBITS 0000076c 00076c 000004 0 A 0 0 4
確かに、.rodata (というかシンボルgsaのアドレスそのもの)です。
% objdump -sj .rodata --start-address=0x70a shared.so shared.so: file format elf32-i386 Contents of section .rodata: 070a 4343 43004444 44004142 43007374 6174 CCC.DDD.ABC.stat 071a 6963 20706f69 6e746572 203d2025 730a ic pointer = %s. 072a 0070 75626c69 6320706f 696e7465 7220 .public pointer 073a 3d20 25730a00 73746174 69632061 7272 = %s..static arr 074a 6179 2020203d 2025730a 00707562 6c69 ay = %s..publi 075a 6320 61727261 79202020 3d202573 0a00 c array = %s..
そこにはちゃんと "CCC" が格納されています。一番目のアクセスでは、ポインタgspを経由して "AAA" にたどり着いたのですが、3番目のこれではダイレクトに "CCC" に到達しています。一番速そうです(本当のところは知りませんが)。
;; const char gpa[] = "DDD"; のアクセス 672: 8b 83 f0 ff ff ff mov 0xfffffff0(%ebx),%eax 678: 89 44 24 04 mov %eax,0x4(%esp)
そろそろ説明を端折ります。non-staticなのでGOTを経由しています。しかし、GOTの指し先は.rodataの"DDD"そのものです。2番目のように、まわりくどくポインタを経由したりはしません。3番目よりはまわりくどいですが、2番目よりは効率が良さそうな気がします。
(2) PIE
次、同じコードにカラのmain()を足して、PIEとしてコンパイルしてみます。gcc -fPIE -pie ですね。できた位置独立な実行ファイルをnmしてみます。すると、変数 g{sp}{ap} の置かれているセクションはおなじっぽいですね。こいつらのアドレスを覚えておきます。
% nm なんちゃら 000007e2 r gsa 000007e6 R gpa 00001858 d gsp 0000185c D gpp 0000193c a _GLOBAL_OFFSET_TABLE_
まえと同じく、eu-readelf -S すると下記。
% eu-readelf -S main_PIE | egrep -e '(got|data|rodata)' [14] .rodata PROGBITS 000007b0 0007b0 00008e 0 A 0 0 4 [19] .data.rel.ro PROGBITS 00001858 000858 00000c 0 WA 0 0 4 [21] .got PROGBITS 0000192c 00092c 000010 4 WA 0 0 4 [22] .got.plt PROGBITS 0000193c 00093c 00001c 4 WA 0 0 4 [23] .data PROGBITS 00001958 000958 000010 0 WA 0 0 4
同じようにbar()を逆アセンブルしてみていきます。
66b: e8 a7 ff ff ff call 617 <__i686.get_pc_thunk.bx> 670: 81 c3 cc 12 00 00 add $0x12cc,%ebx
PICレジスタのセットアップは一緒です。addlする数は、バイナリが異なるのでもちろん微妙に異なるわけですが。
;; static const char* const gsp = "AAA"; のアクセス 676: 8b 83 1c ff ff ff mov 0xffffff1c(%ebx),%eax 67c: 89 44 24 04 mov %eax,0x4(%esp)
これは、PICでのgspのアクセスとまったく一緒です。.data.rel.roのポインタの値をスタックに積んでます。
;; const char* const gpp = "BBB"; のアクセス 68e: 8b 83 20 ff ff ff mov 0xffffff20(%ebx),%eax 694: 89 44 24 04 mov %eax,0x4(%esp)
PIEの1番目、つまりすぐ上のgspの例と同じ、です。PICでは公開された変数であるgppのアクセスはGOTを経由していましたが、PIEではGOTを経由しません。したがって、LD_PRELOADでの乗っ取りはできませんが、コードが若干効率的になります。PIEでは同一実行ファイル内の関数・変数のアクセスではGOTを経由しない決まりになっているようです。
;; static const char gsa[] = "CCC"; のアクセス 6a6: 8d 83 a6 ee ff ff lea 0xffffeea6(%ebx),%eax 6ac: 89 44 24 04 mov %eax,0x4(%esp)
PICの3番目と同じです。効率的なコードです。
;; const char gpa[] = "DDD"; のアクセス 6be: 8d 83 aa ee ff ff lea 0xffffeeaa(%ebx),%eax 6c4: 89 44 24 04 mov %eax,0x4(%esp)
すぐ上、あるいはPICの3番目と同じです。効率的なコードです。
(3) ただのexe
gsp, gpp, gsa, gpa のどのアクセスについても、下記のような安直かつ効率の良さそうなコードになります。
80483e6: c7 44 24 04 18 85 04 movl $0x8048518,0x4(%esp)
と、ここまで書いて思ったのですが、-O2 かけると、それぞれがもちっと効率的なコードになりますね。でも書く気力が尽きたのでいいや。位置独立なコードのいい加減な解説文ということで・・・・。
比較3) 起動時の時間的なコスト(再配置回数)
何の話をしていたのか、完全に忘れた感もありますが、話を戻します。ポインタにするか配列にするかで、プロセス起動時(ロード時)のリロケーションのコストが変化します。PICとPIEで微妙に異なるようなので、分けて。下記挙動はすべて、eu-readelf -r binary で調べられます。このへんは(も)理解が浅いのであっさりです。ちゃんと追わないと駄目だ。。。
(1) PIC
- const char* const x = ""; を使うたびに、R_386_RELATIVE な再配置と R_386_GLOB_DAT な再配置が一つづつ増加します。前者がポインタの指し先の分で、後者がクリリンのぶんというかGOTの分、であってますかね。
- static const char* const x = ""; だと、R_386_RELATIVE が1つ増えるのみです。
- const char x[] = ""; だと、R_386_GLOB_DAT が1つ増えるのみです。
- static const char x[] = ""; だと、ロード時の再配置は起きないようです。
全体的に、配列方式のほうが得ですね。
(2) PIE
- const char* const x = ""; を使うたびに、R_386_RELATIVE が一つづつ増加します。PICの時よりもマシになっています。
- static const char* const x = ""; はstaticなしと同じです。R_386_RELATIVE が1つ。
- const char x[] = ""; だと、ロード時の再配置は起きないようです。
- static const char x[] = ""; でも、(もちろん)ロード時の再配置は起きないようです。
...というわけで、PIEでも配列の勝ちです。
比較4) 空間コスト
ポインタ方式は.dataセクションのポインタ一個分損なんじゃなかろうか。たぶん(やる気がなくなってきた)。
ベンチマーク
ポインタ型/配列型、それぞれのグローバル変数を数百万個含むDSOを作ってみて、
import List main = mapM_ (\x -> putStrLn $ "const char* const "++x++"=\""++x++"\";") $ take 10000000 $ perm ['a'..'k'] where perm [] = [[]] perm xs = concat [map (x:) $ perm (delete x xs) | x <- xs]
まとめ
というわけで、何がいいたいのかよくわからないエントリですが、わたしもわかりません。
とりあえず、可能ならstaticなり __attribute__((visibility("hidden"))) なりは付与するとして、
char* p = "hello"; // 2点 const char* p = "hello"; // 15点: 最適化がろくに効かないしちょっとなー。 // ...っていうかそのポインタ、ほんとに指し変えるの? #define HELLO "hello" // 40点: まぁ、それもアリかもわからんね。-fmerge-all-constants すると吉 const char* const p = "hello"; // 50点: タイプ量が多い割に PIC/PIE だと下のに負ける const char p[] = "hello"; // 70点: イイ!
ってな感じでどうでしょう?
もちろん100点はRubyをつか(ry