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しないとわかんねぇな。