C++のコードを逆アセンブリして遊ぶ
風邪完治かも!
逆アセンブリするC++コードはこれです。
環境は Linux ubuntu-vm 2.6.24-18-generic #1 SMP Wed May 28 20:27:26 UTC 2008 i686 GNU/Linux
解析ツールは IDAPro, gdb
#include <iostream> #include <string> int main(){ std::string s = std::string(4,'A'); std::cout << s << std::endl; return 0; }
C++のコードを初めて逆アセンブリしたとき、うわああああって思った。
原因は関数名が一見めちゃくちゃだったからだ。一応命名規則は存在していて、
丁寧に解析してくれるツールも存在してた。
nmコマンドでシンボル名を調べてくれる。オプションで解析もしてくれる。
以下のシンボル名は関係がありそうなものを抜き出してきたもの。
$ g++ hoge.cpp $ nm a.out 08048980 t _Z41__static_initialization_and_destruction_0ii U _ZNKSs4sizeEv@@GLIBCXX_3.4 U _ZNKSsixEj@@GLIBCXX_3.4 U _ZNSaIcEC1Ev@@GLIBCXX_3.4 U _ZNSaIcED1Ev@@GLIBCXX_3.4 U _ZNSolsEPFRSoS_E@@GLIBCXX_3.4 U _ZNSsC1EjcRKSaIcE@@GLIBCXX_3.4 U _ZNSsD1Ev@@GLIBCXX_3.4 U _ZNSt8ios_base4InitC1Ev@@GLIBCXX_3.4 U _ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4 08048874 t _ZSt17__verify_groupingPKcjRKSs 08048ac6 W _ZSt3minIjERKT_S2_S2_ 08049e80 B _ZSt4cout@@GLIBCXX_3.4 U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@@GLIBCXX_3.4 08049f10 b _ZSt8__ioinit U _ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKSbIS4_S5_T1_E@@GLIBCXX_3.4 $ nm --demagle a.out 08048980 t __static_initialization_and_destruction_0(int, int) U std::string::size() const@@GLIBCXX_3.4 U std::string::operator[](unsigned int) const@@GLIBCXX_3.4 U std::allocator<char>::allocator()@@GLIBCXX_3.4 U std::allocator<char>::~allocator()@@GLIBCXX_3.4 U std::ostream::operator<<(std::ostream& (*)(std::ostream&))@@GLIBCXX_3.4 U std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(unsigned int, char, std::allocator<char> const&)@@GLIBCXX_3.4 U std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()@@GLIBCXX_3.4 U std::ios_base::Init::Init()@@GLIBCXX_3.4 U std::ios_base::Init::~Init()@@GLIBCXX_3.4 08048874 t std::__verify_grouping(char const*, unsigned int, std::string const&) 08048ac6 W unsigned int const& std::min<unsigned int>(unsigned int const&, unsigned int const&) 08049e80 B std::cout@@GLIBCXX_3.4 U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)@@GLIBCXX_3.4 08049f10 b std::__ioinit U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)@@GLIBCXX_3.4
ざっと見ていきましょう。
.text:0804837E lea eax, [ebp+var_9] .text:08048381 mov [esp+40h+var_40], eax .text:08048384 call _ZNSaIcEC1Ev .text:08048389 lea eax, [ebp+var_9] .text:0804838C mov [esp+40h+var_34], eax .text:08048390 mov [esp+40h+var_38], 41h .text:08048398 mov [esp+40h+var_3C], 4 .text:080483A0 lea eax, [ebp+var_10] .text:080483A3 mov [esp+40h+var_40], eax .text:080483A6 call _ZNSsC1EjcRKSaIcE .text:080483AB lea eax, [ebp+var_9] .text:080483AE mov [esp+40h+var_40], eax .text:080483B1 call _ZNSaIcED1Ev _ZNSaIcEC1Ev std::allocator<char>::allocator() _ZNSsC1EjcRKSaIcE std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(unsigned int, char, std::allocator<char> const&) _ZNSaIcED1Ev std::allocator<char>::~allocator()
stringって実はbasic_stringのtypedef らしい。
さてと、順を追っていくと、
[ebp+var_9]のアドレスにデフォルトのallocatorをセット?して、第3引数としてスタックに積む。
0x41('A')を第2引数としてスタックに積んで、第1引数に個数である4をスタックに積んでいる。
でも、よく見てみると、さらに[ebp+var_10]のアドレスがスタックに積まれている。これはstring型変数sを指している。
次の処理としてallocatorをデコンストラクタしている。
allocatorの扱いがよくわかんないww
.text:080483B6 lea eax, [ebp+var_10] .text:080483B9 mov [esp+40h+var_3C], eax .text:080483BD mov [esp+40h+var_40], offset _ZSt4cout .text:080483C4 call _ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKSbIS4_S5_T1_E .text:080483C9 mov [ebp+var_1C], eax .text:080483CC jmp short loc_80483ED _ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKSbIS4_S5_T1_E std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const& 長すぎwww
operator<<にstd::coutと文字列'AAAA'のアドレスを渡している。少し脱線するが、
#include <iostream> int main(){ std::operator<< (std::cout,"AAAA"); return 0; }
みたいにも書けるらしい。
気になったのは、callした後、eaxレジスタの内容を[ebp+var_1C]に格納していること。
operator処理を行う前のeaxレジスタには文字列'AAAA'を指すアドレスが格納されているはずだが、
callを呼んだあとにも同じ値が入ってるのか?…
eaxレジスタは関数の返り値を格納する役割もある。でも、std::coutって返り値あったっけ?
と思ったのがこの記事を書く発端である。調べる過程で、デマングルの存在を知った。
(以前に@ucq氏、@a4lg氏に助言を戴いたことをすっかり失念していたorz)
ので、自分が行ったことと記事の順序が少々前後していて、既に上記の記述の中に答えは出ているが…w
gdbで動的解析してみる。
--------0x08048a3d------- cout前のeaxレジスタを確認 アドレス値が0bfxxxxxxなので、ユーザスタック領域を指している。どうでもいいw (gdb) i r eax 0xbf9f4f08 -1080078584 ecx 0x0 0 edx 0xbf9f4f08 -1080078584 ebx 0xb7e66ff4 -1209634828 esp 0xbf9f4ee0 0xbf9f4ee0 ebp 0xbf9f4f18 0xbf9f4f18 esi 0xb7fb8ce0 -1208251168 edi 0x0 0 eip 0x8048a3d 0x8048a3d <main+77> eflags 0x246 [ PF ZF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 --------0x08048a4d------- cout後のeaxレジスタを確認(ただし、endlはまだ呼ばれてない状態) (gdb) i r eax 0x8049e80 134520448 ecx 0xb7f8672c -1208457428 edx 0x4 4 ebx 0xb7e66ff4 -1209634828 esp 0xbf9f4ee0 0xbf9f4ee0 ebp 0xbf9f4f18 0xbf9f4f18 esi 0xb7fb8ce0 -1208251168 edi 0x0 0 eip 0x8048a4d 0x8048a4d <main+93> eflags 0x286 [ PF SF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51
少し脱線して、cout前のeaxレジスタは何を指していたのかを確認。
(gdb) x/8x 0xbf9f4f08 0xbf9f4f08: 0x0804a014 0x08048b19 0xbf9f4f30 0xb7e66ff4 0xbf9f4f18: 0xbf9f4f88 0xb7d32450 0xb7fb8ce0 0x08048b00 (gdb) x/8x 0x0804a014 0x804a014: 0x41414141 0x00000000 0x00020fe9 0x00000000 0x804a024: 0x00000000 0x00000000 0x00000000 0x00000000
というわけで、eaxレジスタには文字列'AAAA'のアドレスを指すアドレスが格納されてました。
ちなみに、0x0804a000以降はヒープ領域です。確認方法は、プログラム動作中に cat /proc/pid_number/smaps で確認。
0804a000-0806b000 rw-p 0804a000 00:00 0 [heap] Size: 132 kB Rss: 4 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 4 kB Referenced: 4 kB
話を戻しましょう。
cout後のeaxレジスタには 0x8049e80 が格納されてましたが、このアドレスが一体何を指してるのか確認。
(gdb) x/8x 0x8049e80 0x8049e80 <_ZSt4cout@@GLIBCXX_3.4>: 0xb7f8672c 0xb7f86740 0x00000006 0x00000000 0x8049e90 <_ZSt4cout@@GLIBCXX_3.4+16>: 0x00001002 0x00000000 0x00000000 0x00000000
静的解析ではさっぱりわかりませんでしたが、std::coutのアドレスが格納されてるアドレスを指していたようです。
(gdb) x/8x 0xb7f8672c 0xb7f8672c <_ZTVSo+12>: 0xb7f24ad0 0xb7f248f0 0xfffffffc 0xfffffffc 0xb7f8673c <_ZTVSo+28>: 0xb7f86768 0xb7f24ac0 0xb7f248e0 0xb7f8672c さらに指してるアドレスを見たところ、 _ZTVSo が気になったので調べてみた。 動的リンクでコンパイルするとそれらしい名前は発見できなかったので、静的リンクでコンパイルしてみたところ、見つけることができた。 $ g++ -static hoge.cpp $ nm a.out | grep ZTVSo 08107220 V _ZTVSo $ nm --demangle a.out | grep 08107220 08107220 V vtable for std::ostream
うーん…軽くぐぐったが、奥が深そうなので今はスルーしておこう。脱線してるしw
静的リンクでコンパイルして、std::endlの部分を見てみた。
_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ .text:0806F950 push ebp .text:0806F951 mov ebp, esp .text:0806F953 push ebx .text:0806F954 sub esp, 14h .text:0806F957 mov ebx, [ebp+arg_0] .text:0806F95A mov [esp+18h+var_14], 0Ah .text:0806F962 mov eax, [ebx] .text:0806F964 mov edx, [eax-0Ch] .text:0806F967 lea eax, [ebx+edx] .text:0806F96A mov [esp+18h+var_18], eax .text:0806F96D call _ZNKSt9basic_iosIcSt11char_traitsIcEE5widenEc .text:0806F972 mov [esp+18h+var_18], ebx .text:0806F975 movsx eax, al .text:0806F978 mov [esp+18h+var_14], eax .text:0806F97C call _ZNSo3putEc .text:0806F981 mov [ebp+arg_0], eax .text:0806F984 add esp, 14h .text:0806F987 pop ebx .text:0806F988 pop ebp .text:0806F989 jmp _ZNSo5flushEv _ZNKSt9basic_iosIcSt11char_traitsIcEE5widenEc std::basic_ios<char, std::char_traits<char> >::widen(char) const _ZNSo3putEc std::ostream::put(char) _ZNSo5flushEv std::ostream::flush()
0Ahの文字が見えるから改行って感じするけど、flushも呼んでるっぽい。
一番気になってた _ZNSolsEPFRSoS_E(std::ostream::operator<<(std::ostream& (*)(std::ostream&))) を覗いてみたら、
.text:0806D480 push ebp .text:0806D481 mov ebp, esp .text:0806D483 mov ecx, [ebp+arg_4] .text:0806D486 pop ebp .text:0806D487 jmp ecx
しょぼすぎて唖然としたw
_ZNSolsEPFRSoS_Eが呼ばれるの手前
.text:080483ED mov [esp+40h+var_3C], offset _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ .text:080483F5 mov eax, [ebp+var_1C] .text:080483F8 mov [esp+40h+var_40], eax .text:080483FB call _ZNSolsEPFRSoS_E
これを見ると、第1引数にstd::cout、第2引数にstd::endlがスタックに積まれて渡されてる。
なので、_ZNSolsEPFRSoS_Eの処理はarg_4は第2引数を指しているため、std::endlにjmpしてるだけ?いらなくね?…
一応動的解析のほうでも確認しておく。
(gdb) si 0x0806d480 in std::ostream::operator<< () Current language: auto; currently asm (gdb) i r eax 0x8140a00 135531008 ecx 0x810722c 135295532 edx 0x4 4 ebx 0x0 0 esp 0xbfe3415c 0xbfe3415c ebp 0xbfe34198 0xbfe34198 esi 0x20618 132632 edi 0x3 3 eip 0x806d480 0x806d480 <std::ostream::operator<<(std::ostream& (*)(std::ostream&))> eflags 0x382 [ SF TF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) x/8x $esp 0xbfe3415c: 0x08048400 0x08140a00 0x0806f950 0x00000041 0xbfe3416c: 0xbfe3418f 0x08121968 0x0814016c 0xbfe34188 (gdb) x/4x 0x08048400 0x8048400 <main+148>: 0x000000bb 0xf0458d00 0xe8240489 0x0002d540 (gdb) x/4x 0x08140a00 0x8140a00 <_ZSt4cout>: 0x0810722c 0x08107240 0x00000006 0x00000000 (gdb) x/4x 0x0806f950 0x806f950 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_>: 0x53e58955 0x8b14ec83 0x44c7085d 0x000a0424 .text:0806D483 mov ecx, [ebp+arg_4] の処理が終わった後のレジスタの状態を見てみる。 (gdb) si 0x0806d486 in std::ostream::operator<< () (gdb) i r eax 0x8140a00 135531008 ecx 0x806f950 134674768 edx 0x4 4 ebx 0x0 0 esp 0xbfe34158 0xbfe34158 ebp 0xbfe34158 0xbfe34158 esi 0x20618 132632 edi 0x3 3 eip 0x806d486 0x806d486 <std::ostream::operator<<(std::ostream& (*)(std::ostream&))+6> eflags 0x382 [ SF TF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 引数arg_4の値を代入されたecxレジスタを確認してみると、0x806f950 が格納されている。 これは先ほどの確認した std::endl を指すアドレスと一致。まぁ、当然…。 .text:0806D486 pop ebp の処理が終わった後のレジスタの状態 (gdb) si 0x0806d487 in std::ostream::operator<< () (gdb) i r eax 0x8140a00 135531008 ecx 0x806f950 134674768 edx 0x4 4 ebx 0x0 0 esp 0xbfe3415c 0xbfe3415c ebp 0xbfe34198 0xbfe34198 esi 0x20618 132632 edi 0x3 3 eip 0x806d487 0x806d487 <std::ostream::operator<<(std::ostream& (*)(std::ostream&))+7> eflags 0x382 [ SF TF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 ベースポインタを元に戻して、std::endl処理にjmpと。何をしにきたんだって言いたくなるw
2011/1/31 15:02 追記:
ところどころアセンブリのアドレス値がおかしいのは動的リンク時のコードと静的リンク時のコードが混在してるから…っぽい。
申し訳ないorz