Slide 1

Slide 1 text

C言語を知らない人が びっくりしそうな C言語の特徴 Sep. 30th, 2022 Satoru Takeuchi twitter: satoru_takeuchi

Slide 2

Slide 2 text

はじめに ● 想定聴衆 ○ 「C言語は聞いたことはあるがどんなものか知らない」という人 ○ メモリ管理を自分でしなくていいプログラミング言語を使っている人 ■ ここ十数年で生まれた言語はだいたいそう ● はなすこと ○ 想定聴衆に向けてC言語の特徴をいくつか紹介 ○ たくさんある特徴の中から Cを知らない人が面食らいそうなものを抽出

Slide 3

Slide 3 text

C言語の概要 ● C言語はアセンブリ言語と高級言語の間くらいにあるプログラミング言語 ● (アセンブリ言語よりは)可読性が高いコードで高速なプログラムを書ける ● 単純なコードだとあたらしめの言語と見かけ上はそんなに変わらない ● 例: 配列の要素を1行に1つ出力するプログラム C Go func main() { a := []int{0, 1, 2, 3, 4} for i := 0; i < len(a); i++ { fmt.Println(a[i]) } } void main(void) { int i; int a[5] = {0, 1, 2, 3, 4}; for (i=0;i<5;i++) { printf("%d\n", a[i]); } }

Slide 4

Slide 4 text

実行結果 ● 結果は(当然)同じ C Go 0 1 2 3 4 0 1 2 3 4

Slide 5

Slide 5 text

配列の範囲外にアクセスすると… ● C Go func main() { a := []int{0, 1, 2, 3, 4} for i := 0; i < len(a)+1; i++ { fmt.Println(a[i]) } } void main(void) { int i; int a[5] = {0, 1, 2, 3, 4}; for (i=0;i<6;i++) { printf("%d\n", a[i]); } }

Slide 6

Slide 6 text

実行結果 ● ちがう C Go 0 … 4 panic: runtime error: index out of range [5] with length 5 goroutine 1 [running]: main.main() /home/sat/src/youtube-sample/053- surprising-c/outofrange/outofrange .go:10 +0xcb 0 1 2 3 4 0 未定義のa[5]にアクセスしている これは何のデータ? 怒られる

Slide 7

Slide 7 text

理由 ● Go ○ 配列が要素数を示すデータを持っている (“len(a)”でアクセス可能) ○ 範囲外へのアクセスをすると強制終了する ● C ○ 配列は要素数を示すデータを持っていない ○ 配列は単にデータを並べてあるだけ ○ 範囲外にアクセスするとメモリ上の範囲外にあるデータにアクセス可能 ■ 前述の範囲外アクセスだと a[]の隣にあるデータを読んでいる

Slide 8

Slide 8 text

変数とメモリとの関係(Go) a プログラマから見える世界 メモリ 0 1 2 3 4 0 1 2 3 4 ? ? ? ? ? ? ? ? ? ? a変数によるアクセス可能 … … a変数によるアクセス不可 a変数によるアクセス不可

Slide 9

Slide 9 text

変数とメモリとの関係(C) a プログラマから見える世界 メモリ 0 1 2 3 4 0 1 2 3 4 ? ? ? ? ? 0 ? ? ? ? a変数によるアクセス可能 … … さっき見えてしまったところ

Slide 10

Slide 10 text

配列の範囲外に書くとどうなる? void main(void) { int i; int a[5] = {0, 1, 2, 3, 4}; for (i=0;i<6;i++) { a[i] = i * 10; } for (i=0;i<6;i++) { printf("%d\n", a[i]); } } a 0 1 2 3 4 0 1 2 3 4 ? ? ? ? ? ? … … プログラマから見える世界 メモリ ?

Slide 11

Slide 11 text

データ破壊発生 0 10 20 30 40 50 aに関係ないデータが壊れる! a 0 10 20 30 40 0 10 20 30 40 ? 50 ? ? ? ? … … プログラマから見える世界 メモリ ?

Slide 12

Slide 12 text

ポインタ ● 二つの変数a, bを定義して、それぞれの値をポインタ経由で書き換え void main(int argc, char *argv[]) { int a = 1, b = 2; int *p = &a; *p = 10; p = &b; *p = 20; printf("%d, %d\n", a, b); } Go func main() { a := 1 b := 2 p := &a *p = 10 p = &b *p = 20 fmt.Printf("%d, %d\n", a, b) } C

Slide 13

Slide 13 text

ポインタの動作イメージ a プログラマから見える世界 メモリ 1 1 1 2 ? … … b 2 p ? func main() { a := 1 b := 2 p := &a *p = 10 p = &b *p = 20 fmt.Printf("%d, %d\n", a, b) } イマココ

Slide 14

Slide 14 text

ポインタの動作イメージ a プログラマから見える世界 メモリ 10 1 10 2 ? … … b 2 p ? func main() { a := 1 b := 2 p := &a *p = 10 p = &b *p = 20 fmt.Printf("%d, %d\n", a, b) } イマココ

Slide 15

Slide 15 text

ポインタの動作イメージ a プログラマから見える世界 メモリ 10 1 10 2 ? … … b 2 p ? func main() { a := 1 b := 2 p := &a *p = 10 p = &b *p = 20 fmt.Printf("%d, %d\n", a, b) } イマココ ポイント先をaからbに変更!

Slide 16

Slide 16 text

ポインタの動作イメージ a プログラマから見える世界 メモリ 10 1 10 20 ? … … b 20 p ? func main() { a := 1 b := 2 p := &a *p = 10 p = &b *p = 20 fmt.Printf("%d, %d\n", a, b) } イマココ * Cでも基本的には同じ流れになる

Slide 17

Slide 17 text

実行結果 ● おなじになる 10, 20 Go C 10, 20

Slide 18

Slide 18 text

GoとCにおけるポインタの違い ● Go ○ 特定の型(例ではint)の変数のアドレスをポイントできる ○ ポイント先の変数の変更ができるが、それ以外の操作はできない ● C ○ 特定の型(例ではint)の変数のアドレスをポイントできる ○ ポイントする変数の変更ができる ○ ポインタ変数は加減算によってポイントするデータを変更できる

Slide 19

Slide 19 text

ポインタ加算の動作イメージ a プログラマから見える世界 メモリ 1 1 1 2 ? … … b 2 p ? void main(int argc, char *argv[]) { int a = 1, b = 2; int *p = &a; *p = 10; p++; *p = 20; printf("%d, %d\n", a, b); } イマココ ● aとbがメモリ上で必ず連続している保証はないが、ここではそうなっていると仮定 コレナニ?

Slide 20

Slide 20 text

ポインタ加算の動作イメージ a プログラマから見える世界 メモリ 10 1 10 2 ? … … b 2 p ? void main(int argc, char *argv[]) { int a = 1, b = 2; int *p = &a; *p = 10; p++; *p = 20; printf("%d, %d\n", a, b); } イマココ

Slide 21

Slide 21 text

ポインタ加算の動作イメージ a プログラマから見える世界 メモリ 10 1 10 2 ? … … b 2 p ? void main(int argc, char *argv[]) { int a = 1, b = 2; int *p = &a; *p = 10; p++; *p = 20; printf("%d, %d\n", a, b); } イマココ アドレスがint変数ひとつぶん増える !

Slide 22

Slide 22 text

ポインタ加算の動作イメージ a プログラマから見える世界 メモリ 10 1 10 20 ? … … b 20 p ? void main(int argc, char *argv[]) { int a = 1, b = 2; int *p = &a; *p = 10; p++; *p = 20; printf("%d, %d\n", a, b); } イマココ

Slide 23

Slide 23 text

Cで起きがちなポインタまわりの悲劇 ● 誤ったポインタ加減算によって思わぬ問題が発生しうる ○ 想定していたものと違う変数の値を読み書きしてしまう ○ 不正なアドレスにアクセスしてプログラムが強制終了 ● 上記のようなバグを悪用したセキュリティ攻撃を受ける ○ ブラウザ経由でサーバアプリを落とす ○ データを盗む ○ 任意のコードを実行

Slide 24

Slide 24 text

わかったこと ● Cは良くも悪くもメモリの構造がむき出しになっている ● 言語レベルでのデータの保護機能があんまりない ○ 📝 他の言語も意図的にデータ保護機能を切ったりできることもある ● もっといっぱいあるが、多すぎるので省略

Slide 25

Slide 25 text

いまどきCを使いたい場面 ● どうしても高速、軽量なプログラムを作りたい場合 ○ データ保護などの便利機能のためのデータやロジックはなくていい ○ キャッシュメモリなど、ハードウェアを意識したプログラムを作ってもよい ○ ユーザはいまどきの言語を使い、処理系を作る人は Cを使って処理系を書いたり ■ 例: Python処理系はCで書かれている ● カーネルやデバイスドライバ ○ 「特定のメモリアドレスのデータ」を操作しなければならない場面がままある ○ MMIOという特定メモリアドレスにアクセスするとデバイスを操作できるというしくみがある ○ 📝 最近は状況が変わりつつある ■ Linuxはv6.1からRustで一部コードを書けるようになる見込み

Slide 26

Slide 26 text

おわりに ● C言語はアセンブリ言語と高級言語の間くらいにあるプログラミング言語 ● つらいところ ○ 普通に使っていてもメモリの概念がむき出しになっている ○ バグを仕込みやすいし、仕込んだ時にデバッグが難しい ● いいところ ○ (アセンブリ言語よりは )人間に理解しやすい文法で高速なプログラムを書ける ○ メモリを意識せざるをえないデバイスドライバなどを書ける ● 📝 わたし個人の思い ○ 「いいところ」が嬉しい場合は Cを使う ○ それ以外の場合はわざわざ使わなくていい (速度が重要じゃないとか )