C言語の文字列宣言を理解する
前回のエントリで、C言語の文字列宣言によって参照するポインタの位置に違いがある事がわかった。
では、バイナリレベルでGCCがどのように解釈を行っているのかを理解したいと思う。
#include <stdio.h> void hello1(void) { char *s = "Hello, C String World !! Hello, C String World !!"; puts(s); } void hello2(void) { char s[] = "Hello, C String World !! Hello, C String World !!"; puts(s); } int main(void) { int i; for (i = 0; i < 10000000; i++) hello1(); return 0; }
2つの関数の違いはcharポインタの宣言。
- char *s
- char s[]
だけである。
timeで計測する。
1000万回ループを回してtimeで実行時間を計る。
char *s
% time ./hello1 > /dev/null ./hello1 > /dev/null 2.01s user 0.06s system 90% cpu 2.275 total
char s[]
% time ./hello2 > /dev/null ./hello2 > /dev/null 2.55s user 0.26s system 87% cpu 3.199 total
- char *sが、2.275秒
- char s[]が、3.199秒
全く違う宣言であることが確認できる。
objdumpで見比べてみる。
objdump -dしてみる。
080483b4 <hello1>: 80483b4: 55 push %ebp 80483b5: 89 e5 mov %esp,%ebp 80483b7: 83 ec 18 sub $0x18,%esp 80483ba: c7 45 fc 10 85 04 08 movl $0x8048510,0xfffffffc(%ebp) 80483c1: 8b 45 fc mov 0xfffffffc(%ebp),%eax 80483c4: 89 04 24 mov %eax,(%esp) 80483c7: e8 18 ff ff ff call 80482e4 <puts@plt> 80483cc: c9 leave 80483cd: c3 ret 080483ce <hello2>: 80483ce: 55 push %ebp 80483cf: 89 e5 mov %esp,%ebp 80483d1: 83 ec 68 sub $0x68,%esp 80483d4: 8d 4d b5 lea 0xffffffb5(%ebp),%ecx 80483d7: ba 10 85 04 08 mov $0x8048510,%edx 80483dc: b8 4b 00 00 00 mov $0x4b,%eax 80483e1: 89 44 24 08 mov %eax,0x8(%esp) 80483e5: 89 54 24 04 mov %edx,0x4(%esp) 80483e9: 89 0c 24 mov %ecx,(%esp) 80483ec: e8 e3 fe ff ff call 80482d4 <memcpy@plt> 80483f1: 8d 45 b5 lea 0xffffffb5(%ebp),%eax 80483f4: 89 04 24 mov %eax,(%esp) 80483f7: e8 e8 fe ff ff call 80482e4 <puts@plt> 80483fc: c9 leave 80483fd: c3 ret
- char *sの場合.rodada領域からポインタを代入をするだけ。
- char s[]の場合.rodata領域から一生懸命スタック領域へmovでコピーしている。
宣言ひとつ違うだけで、吐き出されるマシン語は全く別物であることが確認できる。
- char *sでは.rodataの値を参照しているだけなので、値の変更をすることが出来ない(OS,CPU,コンパイラによって挙動は異なる)。
- 値を変更する場合は、char s[]としてスタック領域へコピーを行わなければならない。
2つの宣言の違いをはっきり理解出来ました!!
やっぱBinary Hackしないとわかんねぇな。