2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

「この位置にprintfが無いとなぜか動かないんだ。」が面白かったので細かい動作を考えてみる。

Last updated at Posted at 2020-10-28

本日Qiitaのお知らせメールで紹介された(?)こちらの記事。
https://qiita.com/fujitanozomu/items/8b7d9e4e51baf0764dfc

面白かったので、何故そうなるのか細かく考えてみました。

以下のコードは、上記の記事より拝借させて頂きました。

#include <stdio.h>

void fizzbuzz(int n)
{
    int next;
    int i = 1;
    do {
        printf(i % 15 ? i % 5 ? i % 3 ? "%d\n" : "Fizz\n" : "Buzz\n" : "FizzBuzz\n", i);
        if (i++ >= n) next = 0;
    } while (next);
}

int main(void)
{
    printf((char[]){""}); // この位置にprintfが無いとなぜか動かない
    fizzbuzz(100);
}

main関数内のprintfがないと、fizzbuzz関数が100まで出力しなくなる、というもの。
この動作を理解するためには、「呼び出し規約」「スタック」等の知識が必要となります。

詳細は省いてとりあえず答え。

main関数実行直後のsp(スタックポインタ、スタック領域の現在のメモリ上のアドレスを指し示す)が1000であるとすれば、printf実行時のスタックは、以下のようになります。
q3.png

そして、fizzbuzz(100)実行時のスタック(厳密には、fizzbuzz関数のdoの位置)は以下のようになります。
q2.png
詳細は後述しますが、printfを実行した時点で1008番地にはprintfで使用された値が入り、それはprintf実行後に特にクリアされたりはしません。つまり、fizzbuzz関数実行時、next変数は同じ1008番地を見ているため、printf実行時のゴミを参照することになります。
これ故に、printfが実行されるとnextが0以外の値となり、fizzbuzz関数がうまく動作する、というスンポーです。
※もしprintf実行時に、1008番地が0になったとしたら、当然ですがfizzbuzz関数はうまく動作しません。

なんでそうなるの?

これを理解するためには、CPUの仕組みやC言語の仕組みを少し知る必要があります。
※なお、専門家ではないので多少正確でない表現があるかも知れません。突っ込む際は優し目にお願いします。

スタック領域

まずスタック領域について軽く説明させてください。
スタックという名前だけあって、pushで積んでpopで取得(除去)するアレです。
CPUにはメモリの一区画をスタック的に使うための仕組みがあります。SP(スタックポインタ)やPUSH命令、POP命令等です。
※これは設計次第なので、もしかしたらCPUによってはないかもです。ですが、普段我々が使用するパソコン等のCPUにはあるはずです。

スタック領域はあくまで「スタック的に使う」だけなので、「スタックのてっぺん - 4のアドレス」といったアクセスが可能です。

ちなみに、こちらもコンパイラの設計次第ではあるのですが、C言語では通常ローカル変数はスタックに積まれます。
なので、関数内で

int a;
int b;

とかやると、スタックのてっぺんにa用の領域、b用の領域が積まれます。
この時点でSPはb用の領域を指しているので、SP - 4とすることでa用の領域にアクセスできるわけです。
※32bitの場合、intは4バイトなので、「てっぺんの一個前」という意味で-4です。

なお、スタックに積まれたローカル変数は、関数終了時にスタックから除去されます。
※もっと厳密な言い方すると、「スコープを外れたら」ですかね。
 しかも除去はPOPではなく、SPの値を関数呼び出し前の値で書き換える、みたいなことをすることが多いようです。

機械語に関数呼び出し等ない

みなさん馴染みの言語は、多分殆ど「関数」があると思います。
しかし、CPUが理解できる唯一の言語「機械語」には関数はありません。
代わりに(?)CALL命令という物があり、サブルーチンを実行することができます。

プログラムは通常、メモリにロードされ、IP(インストラクションポインタ)が指し示すアドレスにある命令を順に実行していきます。
このIPの値を書き換えれば、別の場所のプログラムを実行することができます。この「別の場所のプログラム」がサブルーチンです。

ただ別のアドレスのプログラムに制御をうつしたいだけならJMP命令というものを使えばよいのですが、飛び先のプログラムが完了したら元の場所に戻ってきたい!なんて場合はCALL命令を使います。JMP命令はIPの値を書き換えるだけですが、CALL命令はIPの値をスタックに積んだ後JMPします。

この仕組みにより、関数を実行できる!ようにも思えるのですが、実は引数の受け渡しと、戻り値の取得は、自前でなんとかしないといけません。

呼び出規約

さて、「引数をどうやって渡す」「戻り値をどうやって受け取る」は、実は色々な方法があります。
この辺りのやり方をルールとして定めたのが「呼び出し規約」です。
実際は、引数・戻り値の他にも、スタックを使用した場合、それを元の状態に戻すのは、関数の呼び出し元?先?というのもあります。
c言語の標準はcdeclというやつですが、ウィンドウズではstdcallという規約を使っていたりします。
cdeclでは、引数はスタックに積む、というルールになっています。

スタックに積んだ後CALL命令を発行するので、先ほどのprintfの例では1000番地に引数、1004番地にreturn addressが格納された訳です。
※なお、1000番地の次が1004番地なのは、32bit命令を想定しているためです。64bit命令ならば、1008になるかと思います。多分。

ちなみに、何ですが、異なる言語のライブラリを使うケースでは、呼び出し規約をあわせないとうまく動作しません。
JavaからCで書かれたプログラムを呼ぶときや、VB.NETからCで書かれたプログラムを呼ぶとき、等です。

結局printfは?

先ほど、printf実行時の1008番地が?のままでした。
printfの引数が二つあれば何事もなく説明完了とできたんですが、実際にメモリの内容を(手で)書き出してみると、それだけでは説明できず、軽く愕然としてこの記事書くの辞めようかと思いました。

しかし、ネットでprintfのコードを見つける事ができたので掲載します。
※printfのコードは、処理系によって異なります。なので、以下はあくまで一例です。

int
__printf (const char *format, ...)
{
 va_list arg;
 int done;

 va_start (arg, format);
 done = vfprintf (stdout, format, arg);
 va_end (arg);

 return done;
}

関数名が__で始まっているのは目をつぶってください。話が長くなる(というか私には説明できない)ので。
このコードではva_list arg;というコードがあるので、これが1008番地に来るはずです。
なので、このargが0以外の値になれば、「このprintfがあると何故か動くんだ」という状態になります。

おしまい

ということで、何とか説明できたんじゃないかな?と思うんですが、如何でしょうか?
この辺り詳しく知りたい方は、「はじめて読む8086」「30日でできる! OS自作入門」等の本で勉強されるとよろしいかと思います。

※ちなみに、SE歴15年程のアプリ屋さんですが、上記知識が役に立ったのは1回だけです。
 しかも、VB.NETからC言語のプログラムを呼ぶ、というケースで担当者が「なんかちゃんと実行されてるんですけどエラーになるんですよね」と言ってきて、「呼び出し規約あってないんじゃないすか」とアドバイスした、という、その程度です…。(実際呼び出し規約の問題でした)

2
2
0

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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?