C言語ポインタ勉強会に参加
オンラインで開催。
少人数制で東工大の先生(uchan_nos)がポインタについてわかりやすく教えてくれるという贅沢な1.5Hでした
typedefでやると関数ポインタがわかりやすくなる
typedef int add_type(int,int); int add(int a, int b); add_type* fp = add; typedef int* int_ptr_type; int_ptr_type arr[10];
関数ポインタがわかりにくい、という人は、ひとまずこれでポインタ型を説明すると、
関数ポインタの演算子の優先度のために無理やり挿入された()
に煩わされずに済むと思います。*1
実例
#include<stdio.h> typedef int add_type(int,int); int add(int a, int b); int main(){ //typedefバージョン。わかりやすい add_type* fp = add; int num = fp(1,3); printf("%d\n",num);//4が出力される //普通の関数ポインタバージョン int (*fp2)(int,int) = add; int num2 = fp2(2,2); printf("%d\n",num2);//4が出力される //普通の関数呼び出し int num3 = add(3,1); printf("%d\n",num3);//4が出力される } int add(int a,int b){ return a+b; }
メモリレイアウト
メモリには
- .text:実行可能,読み取り可能,変更不可能
- .data:実行不可能,読み取り可能,変更可能
- .rodata:実行不可能,読み取り可能,変更不可能
な3領域がある
質問:char配列とcharポインタを文字列リテラルで初期化した時の違いについて
Char型文字配列を文字リテラルで初期化した時と Char型アドレスを文字リテラルで初期化した時の違いですが、
前者は値の変更ができるが、後者は値の変更ができないように思えますが、 これは前者だとヒープ、後者だと静的領域に確保していて変更できないということでしょうか
char test1[] = "test\0"; char *test2 = "test\0"; test1[1] = 't'; puts(test1); test2[1] = 't';//エラー puts(test2);
質問回答
char配列を文字列リテラルで初期化した場合は、スタックに確保される(基本的に関数内のローカル変数はスタックで初期化される)
しかし、char配列を文字列リテラルで初期化した場合は、どこにデータが置かれるかわからないので*2、環境によっては変更不可能なrodataに置かれたり、.dataに置かれたして、変更可能か不可能かは不安定。だからコンパイルエラーにならず実行時エラーになる。
- ちなみに私の環境だとbus errorというものが出る。
- busエラーとは何か?
- busエラーはCの未定義動作でよくでてくるエラー
- ポイント:関数の中でローカル変数を宣言するとスタック変数になる。
- ヒープは関係ない。mallocした時しか使われない
用語
初めて聞くものがいくつかありました。 一般的ではないアカデミックな世界の用語?と最初は思いましたが、ググったら普通にでてくるので一般的なものだと思います*3
- ダブルポインタ:ポインタのポインタのこと。凡例:
**pp
- 参照外し(dereference):ポインタ変数の参照先の値を取ってくること。凡例:
int* p = &num,int x = *p
ダブルポインタの逆アセンブル結果の解説
講義で使われた擬似コードどほぼ同じコードを書いて逆アセンブルしたところ、講師のuchanさんに1行ずつ解説していただけました!
#include<stdio.h> int main(){ int x = 42; int *p = &x; //ポインタ int **pp = &p; //ダブルポインタ printf("%d\n",**pp); //42が出力される }
逆アセンブルしたニーモニックにコメントを振る形で掲載します。
int main(){ push rbp mov rbp,rsp ;準備。説明略 sub rsp,0x20 ;32バイトを引いて、変数が収まる範囲を準備してあげる ;本来なら8バイト変数2つ(`p,pp`)と4バイト変数一つ(`x`)の合計20バイト分で十分だが、 ;アライメントの関係で16倍の数値で確保している lea rdi,[rip+0x57] # 100000fa6 <_main+0x66> lea rax,[rbp-0x10];raxにはpのアドレスが lea rcx,[rbp-0x4];rcxにはxのアドレスが入ってる ;※leaはメモリから読みだすという操作をしない。のでrbp-0x4のアドレスがレジスタそのまま入る ;lea命令はコンパイラがよく使う ;余談:lea命令を使うと複雑な命令が1命令でできたりする。(掛け算とか) ;int x = 42; mov DWORD PTR [rbp-0x4],0x2a ;rbpはスタックのベースポインタ ;アドレスは低い方に成長していく。 ;スタックの最上位に4バイトのデータ(int型)であるxを入れる。 ;int *p = &x; mov QWORD PTR [rbp-0x10],rcx ;pのアドレス(rbp-0x10)の先に、xのアドレスを代入する ;注:アドレスのサイズは8バイト ;int **pp = &p; mov QWORD PTR [rbp-0x18],rax ;スタックの一番最下層に,`**pp`を確保しそこにrax(pのアドレスを格納する) ;printf("%d\n",**pp); mov rax,QWORD PTR [rbp-0x18] ;pのアドレスがraxに入る, **(pp) mov rax,QWORD PTR [rax] ;xのアドレスがraxが入る *(*pp) mov esi,DWORD PTR [rax] ;4バイト読みだしてesi (**p) ;末尾の()付きの部分が実行されたイメージ。 mov al,0x0 call 100000f84 <_main+0x44> xor esi,esi }
一応逆アセンブル結果の全体を貼っておきます
念のため。僕が作ったデタラメな擬似コードじゃないよ〜という意味で。
$ gobjdump -d -S -M intel ./a.out ./a.out: ファイル形式 mach-o-x86-64 セクション .text の逆アセンブル: 0000000100000f40 <_main>: #include<stdio.h> int main(){ 100000f40: 55 push rbp 100000f41: 48 89 e5 mov rbp,rsp 100000f44: 48 83 ec 20 sub rsp,0x20 100000f48: 48 8d 3d 57 00 00 00 lea rdi,[rip+0x57] # 100000fa6 <_main+0x66> 100000f4f: 48 8d 45 f0 lea rax,[rbp-0x10] 100000f53: 48 8d 4d fc lea rcx,[rbp-0x4] int x = 42; 100000f57: c7 45 fc 2a 00 00 00 mov DWORD PTR [rbp-0x4],0x2a int *p = &x; 100000f5e: 48 89 4d f0 mov QWORD PTR [rbp-0x10],rcx int **pp = &p; 100000f62: 48 89 45 e8 mov QWORD PTR [rbp-0x18],rax printf("%d\n",**pp); 100000f66: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 100000f6a: 48 8b 00 mov rax,QWORD PTR [rax] 100000f6d: 8b 30 mov esi,DWORD PTR [rax] 100000f6f: b0 00 mov al,0x0 100000f71: e8 0e 00 00 00 call 100000f84 <_main+0x44> 100000f76: 31 f6 xor esi,esi } 100000f78: 89 45 e4 mov DWORD PTR [rbp-0x1c],eax 100000f7b: 89 f0 mov eax,esi 100000f7d: 48 83 c4 20 add rsp,0x20 100000f81: 5d pop rbp 100000f82: c3 ret セクション __TEXT.__stubs の逆アセンブル: 0000000100000f84 <__TEXT.__stubs>: 100000f84: ff 25 86 00 00 00 jmp QWORD PTR [rip+0x86] # 100001010 <_main+0xd0> セクション __TEXT.__stub_helper の逆アセンブル: 0000000100000f8c <__TEXT.__stub_helper>: 100000f8c: 4c 8d 1d 75 00 00 00 lea r11,[rip+0x75] # 100001008 <_main+0xc8> 100000f93: 41 53 push r11 100000f95: ff 25 65 00 00 00 jmp QWORD PTR [rip+0x65] # 100001000 <_main+0xc0> 100000f9b: 90 nop 100000f9c: 68 00 00 00 00 push 0x0 100000fa1: e9 e6 ff ff ff jmp 100000f8c <_main+0x4c> セクション __TEXT.__unwind_info の逆アセンブル: 0000000100000fac <__TEXT.__unwind_info>: 100000fac: 01 00 add DWORD PTR [rax],eax 100000fae: 00 00 add BYTE PTR [rax],al 100000fb0: 1c 00 sbb al,0x0 100000fb2: 00 00 add BYTE PTR [rax],al 100000fb4: 00 00 add BYTE PTR [rax],al 100000fb6: 00 00 add BYTE PTR [rax],al 100000fb8: 1c 00 sbb al,0x0 100000fba: 00 00 add BYTE PTR [rax],al 100000fbc: 00 00 add BYTE PTR [rax],al 100000fbe: 00 00 add BYTE PTR [rax],al 100000fc0: 1c 00 sbb al,0x0 100000fc2: 00 00 add BYTE PTR [rax],al 100000fc4: 02 00 add al,BYTE PTR [rax] 100000fc6: 00 00 add BYTE PTR [rax],al 100000fc8: 40 0f 00 00 rex sldt WORD PTR [rax] 100000fcc: 34 00 xor al,0x0 100000fce: 00 00 add BYTE PTR [rax],al 100000fd0: 34 00 xor al,0x0 100000fd2: 00 00 add BYTE PTR [rax],al 100000fd4: 84 0f test BYTE PTR [rdi],cl 100000fd6: 00 00 add BYTE PTR [rax],al 100000fd8: 00 00 add BYTE PTR [rax],al 100000fda: 00 00 add BYTE PTR [rax],al 100000fdc: 34 00 xor al,0x0 100000fde: 00 00 add BYTE PTR [rax],al 100000fe0: 03 00 add eax,DWORD PTR [rax] 100000fe2: 00 00 add BYTE PTR [rax],al 100000fe4: 0c 00 or al,0x0 100000fe6: 01 00 add DWORD PTR [rax],eax 100000fe8: 10 00 adc BYTE PTR [rax],al 100000fea: 01 00 add DWORD PTR [rax],eax 100000fec: 00 00 add BYTE PTR [rax],al 100000fee: 00 00 add BYTE PTR [rax],al 100000ff0: 00 00 add BYTE PTR [rax],al 100000ff2: 00 01 add BYTE PTR [rcx],al