LoginSignup
35
35

More than 5 years have passed since last update.

アセンブリ言語のさわりを知ればC言語のポインタもわかるよね、という話

Posted at

前置き

大学での初心者に対するプログラミング講義ではC言語を使うべきでない
というお話がありまして、「たしかになぁ」と思いました。
で、Qiitadon にいろいろとトゥートしてまして、なんか微妙に語弊がある部分もあったりしたのでまとめてみよう、という話です。

結論

  • C言語はたしかに初学者には難しい。
  • プログラミング講義ではプログラミングに特化した言語を使用すべき
  • C言語はコンピューターを理解する意味では適切。でもコンピューターの動きの基礎を理解してからやるべき。

ということです。

Qiitadonのトゥートとしては以下のあたりです。

「プログラミングを覚える」と「コンピューターの動きを覚える」は別問題だとは僕も思う。少なくともC言語を入門にす必然性は無いだろうし、カリキュラムの問題なんだろうなあ、とか考えたりはした。

20年ならC言語一択だったけど、今ならPythonだよねぇ。
昔はそれこそ、「覚えやすくてつぶしが利く」からC言語だったわけでしょ。
その位置にあるのは、今ならPythonかなと。

いやま実際の話、なんらかのアセンブリ言語を学習すれば、C言語のポインタはすぐ理解できるんだよね。メモリの置き換えでしかないわけで。
「Javaスクールの危険 - The Joel on Software Translation Project」 http://local.joelonsoftware.com/wiki/Javaスクールの危険
を提示する方もいた気がするけど、そこでふるい分けする必要もなく、順序立てて教えればポインタも再帰も学習できる。
野球部に入っていきなり「素振り1000回」とか言われて上達するとは思えなくて、まずは(大抵の人は部活に入る前に体験するんだろうけど)野球をやって楽しさを味わうでしょ、
もっと上手くなりたいって思うから素振りするんでしょ、
とかは思いますわね。
学習法でどうにかなる部分はサクッとどうにかなって欲しいところだす。

アセンブリ言語を学習すればポインタはすぐわかるよ、という話

C言語の最大の難関といえば、ポインタですよね。
これは、いきなりC言語から始めるのでわかりづらいので、アセンブリ言語、機械の気持ちになった言葉で考えればすぐわかる話なのです。

すごく簡易的な例を挙げてみます。

test.c
void main() {
    int a;
    int b;

    b = 1;
    a = b;
}

こんなC言語のプログラムがあったとしましょう。
これを、gcc -S test.c とやります。
-Sは、

-S Compile only; do not assemble or link

という意味です。C言語をコンパイルはするけどアセンブラで機械語にするところまではやらないよ、と。
つまりアセンブリ言語で出力されます。
出力される拡張子は.sです。

test.s
        .arch armv6
        .eabi_attribute 27, 3
        .eabi_attribute 28, 1
        .fpu vfp
        .eabi_attribute 20, 1
        .eabi_attribute 21, 1
        .eabi_attribute 23, 3
        .eabi_attribute 24, 1
        .eabi_attribute 25, 1
        .eabi_attribute 26, 2
        .eabi_attribute 30, 6
        .eabi_attribute 34, 1
        .eabi_attribute 18, 4
        .file   "test.c"
        .text
        .align  2
        .global main
        .type   main, %function
main:
        @ args = 0, pretend = 0, frame = 8
        @ frame_needed = 1, uses_anonymous_args = 0
        @ link register save eliminated.
        str     fp, [sp, #-4]!
        add     fp, sp, #0
        sub     sp, sp, #12
        mov     r3, #1
        str     r3, [fp, #-8]
        ldr     r3, [fp, #-8]
        str     r3, [fp, #-12]
        sub     sp, fp, #0
        @ sp needed
        ldr     fp, [sp], #4
        bx      lr
        .size   main, .-main
        .ident  "GCC: (Raspbian 4.9.2-10) 4.9.2"
        .section        .note.GNU-stack,"",%progbits

はい、キモいですね
実際には、「main:」の前までは設定をしているだけなのであまり意識する必要はありません。
@はコメントなので、実際には10行程度のプログラムです。

続いて、ポインタを使ったプログラムも用意します。

test2.c
void main() {
    int *a;
    int b;

    b = 1;
    a = &b;
}

アセンブリ言語としてはこうです。

test2.s
        .arch armv6
        .eabi_attribute 27, 3
        .eabi_attribute 28, 1
        .fpu vfp
        .eabi_attribute 20, 1
        .eabi_attribute 21, 1
        .eabi_attribute 23, 3
        .eabi_attribute 24, 1
        .eabi_attribute 25, 1
        .eabi_attribute 26, 2
        .eabi_attribute 30, 6
        .eabi_attribute 34, 1
        .eabi_attribute 18, 4
        .file   "test2.c"
        .text
        .align  2
        .global main
        .type   main, %function
main:
        @ args = 0, pretend = 0, frame = 8
        @ frame_needed = 1, uses_anonymous_args = 0
        @ link register save eliminated.
        str     fp, [sp, #-4]!
        add     fp, sp, #0
        sub     sp, sp, #12
        mov     r3, #1
        str     r3, [fp, #-12]
        sub     r3, fp, #12
        str     r3, [fp, #-8]
        sub     sp, fp, #0
        @ sp needed
        ldr     fp, [sp], #4
        bx      lr
        .size   main, .-main
        .ident  "GCC: (Raspbian 4.9.2-10) 4.9.2"
        .section        .note.GNU-stack,"",%progbits

ざっくり、相違点だけ抜き出してみますと、

test.s(抜粋)
        mov     r3, #1
        str     r3, [fp, #-8]
        ldr     r3, [fp, #-8]
        str     r3, [fp, #-12]

と、

test2.s(抜粋)
        mov     r3, #1
        str     r3, [fp, #-12]
        sub     r3, fp, #12
        str     r3, [fp, #-8]

になります。
ここだけ解説してみましょう。

前提としては、

とします。
こんな説明じゃわからないとは思いますが、正直多くの人には必要ないので壮大に割愛します。

では、相違点の解説です。

    mov     r3, #1

数字の1をr3にコピー(mov)します。
このr3というのは、レジスタと呼ばれるもので、CPUが大抵持ってる「記録できる場所」です。

   str     r3, [fp, #-8]

r3にコピーした値を、fpの8バイト手前に代入します。

   ldr     r3, [fp, #-8]

今代入した値をもういっかいr3に読みとります。

   str     r3, [fp, #-12]

r3にコピーした値を、fpの12バイト手前に代入します。

ということをやっているわけです。

対して、ポインタを使用した場合はこうなります。

   mov     r3, #1

数字の1をr3にコピー(mov)します。

   str     r3, [fp, #-12]

r3にコピーした値を、fpの12バイト手前に代入します。

   sub     r3, fp, #12

fpアドレスから12バイトを引いたアドレスをr3に代入します。

   str     r3, [fp, #-8]

r3にコピーした値をfpの8バイト手前に代入します。

つまり、

今代入した値をもういっかいr3に読みとります。

か、

fpアドレスから12バイトを引いたアドレスをr3に代入します。

かの違いなわけです。

ただ変数を代入したプログラムでは、「値を読んで値を書き込む」という処理だったのが、
ポインタを使うと「アドレスを読んでアドレスを書き込む」という処理に変わるわけです。

そのあたりの勘所さえつかめれば、C言語のポインタも理解できると思いますし、
C言語でポインタを使うときは「メモリの気持ちになって考える」ことが大事だよ、ということがわかりますね。

以上のレベルのことまで教えた上でC言語を学習するのと、いきなりプログラミング講義でC言語を使用するのとでは理解度も変わってくるのではないでしょうか。

世の大学講師には、是非ともPythonでのプログラミング講義を実施していただきたいと思います!(唐突)

35
35
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
35