ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

C言語のstrtok関数でSEGVが起きてハマった話

スポンサーリンク

唐突にC言語のネタ
というのも最終的にCで書くことになるから、最初からCで書いとくか、、、、と言う話になって、そこでハマったから(T_T)
C言語力が足りない(T_T)

C言語strtokでハマった

経緯は冒頭に書いたとおりで、とりあえずCでやりたい処理を実装して、簡単なテストを動かすか、、、ということで以下のようなことをしようとした。(因みにいろいろ端折ってます。)
まあ、要はstrtok使って入力用として置いたテキストデータをいい感じに構造体に突っ込もうとしたんですな。
以下のコードを見ただけで「あ〜〜、これはアカンわ、、、」と気づける方々は、ここで読むのを止めて問題ないっす。 #なお、いろいろ穴があるのには目を瞑ってくださいm( )m。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct data_s {
  int idx;
  char* data;
} data_t;


int main(void)
{
    int i;

    char* tok = ":";
    char* tok_ptr;
    char* test_line_data[] = {
      "0:01234567",
      "1:082acb4567",
      "2:0ab12ff34567",
    };

    data_t *test_data[100];

    for (i = 0; i < sizeof(test_line_data)/sizeof(test_line_data[0]); i++) {
        test_data[i] = (data_t*)malloc(sizeof(data_t));

        tok_ptr = strtok(test_line_data[i], tok);
        test_data[i]->idx = atoi(tok_ptr);

        tok_ptr = strtok(NULL, tok);
        test_data[i]->data = (char*)malloc(strlen(tok_ptr)+1);
        strcpy(test_data[i]->data, tok_ptr);

        printf("test_data[%d]->idx = %d\n", i, test_data[i]->idx);
        printf("test_data[%d]->data = %s\n", i, test_data[i]->data);
    }

    return 0;
}

このコードをコンパイルして実行すると、以下のようにSEGVで落ちる。

diningyo@diningyo-pc:~$ gcc -o strtok_segv strtok_segv.c
diningyo@diningyo-pc:~$ ./strtok_segv
Segmentation fault

原因調査

イマイチ理由がわからずに、いろいろ右往左往するはめに。。こういう時にちゃんと勉強してないのが如実に現れる。。。

とりあえずいろいろ試してみてtest_line_dataの宣言を以下の様に2次元配列にすると問題ないことが分かった。

    char test_line_data[][100] = {

そして判明

上記の宣言の違いによる変化を念頭においていろいろ探してみたところ、以下のブログ記事にたどり着いた。

tamacona.hatenablog.com

引用させていただくと、、

一方、 c char *str = "hogehoge"; …(2) のときは、コンパイルされた時点で、"hogehoge"という文字列リテラルが実行ファイル内に設定されています。そして、関数が呼び出されたときに、ポインタ変数strが文字列リテラルの先頭アドレスで初期化されます。 この文字列リテラルは、リードオンリーデータ領域(.rodata)という、読み取りのみ可能な領域に存在します。 つまり、 c char str[] = "hogehoge"; …(1) は、読み書き可能なスタック領域に文字列が存在するので、文字列を書き換えできる。 c char *str = "hogehoge"; …(2) は、リードオンリーな領域を指し示しているので、書き換えようとするとBus errorとなる。 ということです。

とのことだった。なるほど〜〜〜!!
とても参考になりました。ありがとうございますm( )m

今回の自分のケースに置き換えて考えると、ポインタ版(*test_line_data[]版)ではただ単に配列の各要素のポインタには.rodataのアドレスが入っていて、そこは.rodata領域なのでライトが出来ない状態になっている。
その領域を指しているポインタをそのままstrtokに渡して処理した際に、strtok内の処理で区切り文字を\0に置き換えるためにライトした結果プログラムが落ちた、というのが真相のようだ。

確認してみる

そういうことならアセンブラ見れば違いがあるよね!!ってことで確認をしてみた。

diningyo@diningyo-pc:~$ gcc -o strtok_segv strtok_segv.c -g
diningyo@diningyo-pc:~$ objdump -S -D ./strtok_segv > strtok_segv.log

上記でアセンブラをダンプして、中身を確認してみたのが以下のものになる。

    char* test_line_data[] = {
  40071c:       48 c7 85 a0 fc ff ff    movq   $0x400936,-0x360(%rbp)
  400723:       36 09 40 00
  400727:       48 c7 85 a8 fc ff ff    movq   $0x400941,-0x358(%rbp)
  40072e:       41 09 40 00
  400732:       48 c7 85 b0 fc ff ff    movq   $0x40094e,-0x350(%rbp)
  400739:       4e 09 40 00
      "2:0ab12ff34567",

上記のmovq命令の$0x400936付近のダンプデータは以下↓で、見ての通り.rodata領域でした。

セクション .rodata の逆アセンブル:

0000000000400930 <_IO_stdin_used>:
  400930:       01 00                   add    %eax,(%rax)
  400932:       02 00                   add    (%rax),%al
  400934:       3a 00                   cmp    (%rax),%al
  400936:       30 3a                   xor    %bh,(%rdx)
  400938:       30 31                   xor    %dh,(%rcx)
  40093a:       32 33                   xor    (%rbx),%dh
  40093c:       34 35                   xor    $0x35,%al
  40093e:       36 37                   ss (bad)
  400940:       00 31                   add    %dh,(%rcx)
  400942:       3a 30                   cmp    (%rax),%dh
  400944:       38 32                   cmp    %dh,(%rdx)

比較用にtest_line_data [][100]で宣言したものもペタリ。

    char test_line_data[][100] = {
  40071c:       48 8d 85 90 fb ff ff    lea    -0x470(%rbp),%rax
  400723:       ba c0 09 40 00          mov    $0x4009c0,%edx
  400728:       b9 25 00 00 00          mov    $0x25,%ecx
  40072d:       48 89 c7                mov    %rax,%rdi
  400730:       48 89 d6                mov    %rdx,%rsi
  400733:       f3 48 a5                rep movsq %ds:(%rsi),%es:(%rdi)
  400736:       48 89 f2                mov    %rsi,%rdx
  400739:       48 89 f8                mov    %rdi,%rax
  40073c:       8b 0a                   mov    (%rdx),%ecx
  40073e:       89 08                   mov    %ecx,(%rax)
  400740:       48 8d 40 04             lea    0x4(%rax),%rax
  400744:       48 8d 52 04             lea    0x4(%rdx),%rdx

あんまりちゃんとは読めないんですが、ポインタ版はただ単に.rodataにあるデータのアドレスが格納されてるだけなのに対して、2次元配列版ではrep movsqでデータをコピーしてる感じになってました。
データをコピーして持ってくるため、2次元配列版では通常のライト可能な領域にデータが置かれておりstrtokで問題なく処理が出来た、、、と。
そんなわけで、データがどこに置かれるか、という点と各関数での処理でデータを変更するか、という点には改めて注意せねば、、と感じた次第です。