Yabu.log

ITなどの雑記

C言語ポインタ勉強会に参加

オンラインで開催。

f:id:yuyubu:20180811224641p:plain

atnd.org

少人数制で東工大の先生(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

*1:いずれどこかで読み解く必要はありますが。

*2:.textエリアには置かれないと思うが

*3:私はC言語は入門書を一冊読んだだけなので語彙が少ない。