ichirin2501's diary

いっちりーん。

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