唐突に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] = {
そして判明
上記の宣言の違いによる変化を念頭においていろいろ探してみたところ、以下のブログ記事にたどり着いた。
引用させていただくと、、
一方、
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
で問題なく処理が出来た、、、と。
そんなわけで、データがどこに置かれるか、という点と各関数での処理でデータを変更するか、という点には改めて注意せねば、、と感じた次第です。