2014/07/14

ポインタの話をしていたらいつの間にか 配列の初期化 とか glibc の printf とかを読んでいた話

2014/07/13 にとつぜん @y0t4 さんと「ポインタを一年生にどう解説するか」みたいな話をしてたと思ったら想定外に長話 + 長旅になったのでつらつらと。


char* の初期化の話

C で char* を初期化する時に

とかするじゃん、という話になって、これはどう動いているかというと
  • (char *) の hoge が作られる
  • "" なのリテラルがメモリに展開される
  • その先頭アドレスが hoge に代入される
という話になって、それじゃ実証しよう、ということで 
  • a.out を objdump -d
  • main とか探すと
    •   4004cc:       48 c7 45 f8 e8 05 40    movq   $0x4005e8,-0x8(%rbp)
  • とかしてるので、 0x4005e8 くらいが怪しいだろう、と
  • objdump -D して  0x4005e8  を見ると
    • 4005e8:       66                      data16
    • 4005e9:       75 67                   jne    400652 <__dso_handle+0x72>
    • 4005eb:       61                      (bad)  
  • となっているので "fuga" っぽい。
    • [2] pry(main)> 'fuga'.chars.map{|c| c.ord.to_s(16)}
    • => ["66", "75", "67", "61"]
  • ちなみにこのアドレスは .rodata っぽいっすね。
  • ということで、 "" は一旦どこぞに展開されてからその先頭アドレスが返る、という認識であってるやー、という一段落


char[] の初期化の話

とまでなったところで、じゃあいわゆる「ポインタなんて配列とほとんど同じだよー」なんて発言に対抗することに。
char[] の初期化だと

とかするじゃん、ということで char* の初期化との違いは?ってなことに。
実行すると fuga が出ることは同じなんだけれど。
  • a.out を objdump -d してみると
    • 4004c8:       48 83 ec 10             sub    $0x10,%rsp
    • 4004cc:       c6 45 f0 66             movb   $0x66,-0x10(%rbp)
    • 4004d0:       c6 45 f1 75             movb   $0x75,-0xf(%rbp)
    • 4004d4:       c6 45 f2 67             movb   $0x67,-0xe(%rbp)
    • 4004d8:       c6 45 f3 61             movb   $0x61,-0xd(%rbp)
    • 4004dc:       c6 45 f4 00             movb   $0x0,-0xc(%rbp)
  • こっちは "fuga" を register に入れてってる
  • ということで配列の初期化は要素が1個1個入れられていってる
  • "" みたいに data として確保されるわけじゃなくて、各要素ごとに処理をしている
ということに。
なので char* はあくまで変数は char* のみで、そこに rodata にある "" のアドレスが入ってる。
char[] は array として確保されてる領域に 1つ1つ入れてる。

ちなみに私は C に string って無いから "" の syntax 自体無いものだと思い込んでいて、内部的に char[] に変換されるような拡張が gcc とかにあるようだと思ってたけれどオプションでは見つけられなかった。
どうなってるかは C99 の仕様とかを読んだ方が良いのかも。


printf の呼び方

ということで、 "" は実際は先頭アドレスとして扱える、というところまで来て、 printf に渡す "" はどうなってるの、ということに。
結局 "" で渡しているので、の中身本体は rodata にあるのだけれど、printf 関数に渡してるのは pointer だから、pointer 渡せるのでは、ということでやってみる。

動く上に当然ながら第一引数の中に %p とかが入っていても展開される。
となった時に、第二引数消したらどうなるの、ということに。
-Wall すると「printf に引数足りないよ」って言われるけれど、ポインタで渡してるからそんなのチェックしてないだろうし、ということでこんなコード。

そして実行。すると毎回違う値が出てくる。
指定してる先が無いはずなのに nil とかじゃなくて違う値が出てくるのはどうして、ということになった。


そして glibc へ

じゃあ printf 読もうぜ、ということになって glibc を読むことに。
yota さんが「最近 glibc make した」とのことで手元にソースが。
printf は stdio.c の中にあると思ったらそうでもなく、  stdio-common にありました。

  • ptintf.c がある
    • vfptinrf に stdout を渡してる
    • たぶん va_start が可変長引数なんだろうなー
  • vfprintf.c に vfprintf がある
    • このあたりから大量のマクロが
      • vim の syntax highlighter の色が変
      • 何気に tabstop=8 の世界
      • あとマクロなので設定上1行
      • これメンテ大変なのでは
    • そして読んでると jump_table ってものが
    • 見るとこれが %s とかの分岐先らしい
    • つまり jump_table にあるのが printf の format option だということっぽい
    • ちなみに JUMP_TABLE_TYPE に飛び先があるっぽいですね
      • これによると REF form_pointer へ jump するっぽい
  • LABEL (form_pointer): から奥
    • fspec の値によって va_arg から取ってくるか args_value から取ってきて ptr に入れる
      • va_arg の方は読んでない
      • たぶん args_value から取ってくるだろう、と
        • args_value は strcut printf_arg
        • これが pa_pointer を持ってる
        • ってことでこいつがポインタっぽいなー
        • void * だし
    • ちなみに ptr が null だったら (nil) って表示するっぽくて
      • ptinrf("%p", NULL);
      • ってやると (nil) って出てきた
      • やったね
    • ptr が NULL じゃなかったら 0x ... って出力してる
      • ちなみに full width character とのかねあいなのか、 is_long とかってオプションが
      • glibc 2.19 だと is_long = 0 だった
      • HEAD だとちゃんと処理が書かれてた
      • blame すると 1995年とかのもある
      • 2014年現在メンテされてるのすごい
    • fspec のはなし
      • prosess_arg って macro の引数
      • 呼んでるのは vfprintf.c の
      • 1635:          process_arg (((struct printf_spec *) NULL));
      • 2004:            process_arg ((&specs[nspecs_done]));  
      • とか
        • たぶん NULL のやつが一番最初の実行。
        • nspecs_done のやつは続いてく処理っぽい。
        • つまり1引数処理が終わると ++nspecs_done されていく
      • specs ってのが struct printf_spec
        • それが nspecs 分ある
        • その n は割と適当で、32とか決め打ちされてた
        • 領域足りなかったらもう一回 alloca とかしてるっぽい
  • つまりどういうことだってばよ
    • fspecs に alloca して printf_arg が入っていく
    • 引数を処理していくと nspecs_done が増えていく
ということで、毎回出力が違うのは alloca したメモリ領域の zero fill されていないゴミの値だろう、という結論に。
つまり前の値が書き変わってなければ同じものが出るんじゃないか、ということで %p を多くしてみる。

そうすると毎回同じ値が出力されるようになりました。
やっぱり確保した領域のゴミっぽいですね

つまり、可変長引数を扱うためにメモリはとりあえず確保しとかないといけなくて、処理した値はそこに入れられる、と。
メモリの値は0にする保証とかは無いので、処理が無ければメモリに入ってた値がそのまま出ちゃう、ということっぽいですね。

書いちゃえばそりゃーそーか、ってなる話なのだけれど、引数があることを想定した段階で関数はその処理を書かざるを得なくて、その処理の副産物みたいなのが垣間見えました。


glibc 読んでてな小ネタ

  • リアル : 「ここを消すと動かなくなる」
    • printf_arg が、マクロの中で宣言されていなくて外で宣言されていて
    •       union printf_arg *args_value;>/* This is not used here but ... */
    • とか書かれてました。
  • do { ... } while(0)
    • なんでこれ括られてるのー、ってなってた
    • たぶんスコープが欲しかったんだろう
    • でも後から { ... } だけの部分とかあった
    • たぶん昔は do 書かないと怒られたんだろう
  • バッファが溢れないようにするには
    • ポインタを説明する時に size 調べて malloc するか、 char hoge[256] とかやった方が良いのか、って話してた最中
    • サイズが足りてなかったら 2倍 alloca する、みたいなルーチンが fspecs のところに
    • とりあえず適当に確保して足りなかったら増やせばええんやー、って
  • 関数だと思ったらマクロだった
    • process_arg ((&specs[nspecs_done]));  
    • prosess_arg って関数が無い
    • マ、マクロだー
    • マクロのメンテって大変なのでは
  • printf とかがあるからそりゃ glibc の依存度ヤバいのでは
    • というか printf がエンバグしたら全部アカンのでは
    • でもメンテしてるのすごい

そんなこんなで気付いたら

3時間ぐらい glibc とかと戦ってました。
このブログも書くのに3時間ぐらい。
いやでもなんだかんだ読んでてこうなるんじゃね、ってのを予想して実行してそれが出た時にはうおーってなりましたまる

最初のポインタについて話をしていは部分は yota さんがまとめてくれたっぽいのであうとそーしんぐ

2 件のコメント:

  1. 2年前の記事に何ですが……
    >do { ... } while(0)
    >なんでこれ括られてるのー、ってなってた
    中にbreakがあったんじゃないですか?

    返信削除
  2. > 2年前の記事に何ですが……
    はい。私も少々驚きました。コメントありがとうございます。
    二年前の記憶はほとんど残っていなかったので 'while (0)' で glibc に git grep をかけてみたのですが、 do while(0) を break する do while 0 trick というものがあるようです。
    https://github.molgen.mpg.de/git-mirror/glibc/blob/20003c49884422da7ffbc459cdeee768a6fee07b/iconv/loop.c#L193


    > 中にbreakがあったんじゃないですか?
    おっしゃる通り、do の中で break することでエラーハンドリングをしやすくできるようです。
    http://qiita.com/ymko/items/ae8e056a270558f7fbaf
    実際いくつか見たところブロックの中に break が入っているものがありました。
    また、他にもマクロで複数行の処理を書く場合には do while で囲っておいた方が syntax 的に安全なようです。
    http://stackoverflow.com/questions/257418/do-while-0-what-is-it-good-for

    当時の私がどの部分を読んでいたのか忘れてしまいましたが、スコープを作る以外にも意味があることが分かりました。ありがとうございます。

    返信削除