static 関数や static 変数をヘッダーに2015年09月19日 12時40分29秒

何故だか知らないが、チョコチョコ見付ける摩訶不思議な関数と変数。static 関数や static 変数が .h のヘッダファイルに書かれているのを良く見付ける。あちこちで見たことがあるし、消しても消しても誰かが復活させる。

static 関数や static 変数をヘッダファイルに書くのは大間違い。

C++ ではあれこれと新しい意味も増えた。基本の C 言語由来の static はこうだ。static 修飾子を付けると、関数や変数がコンパイル単位内でしか、見えなくなるのだ。extern を付けて、広域で使い回すのと逆になる。

さて、問題はここからだ。ヘッダーファイルのコンパイル単位はそれを #include する各々のファイルだ。何が起きるのかというと、ヘッダーファイルに書かれた static 関数と static 変数は各々のオブジェクトファイルに生成される。

つまり、ヘッダーファイルに書かれた static 関数と static 変数はそれを読み込むファイルの数だけ生成される事になる。同じ関数がいくつも定義されているのに何故エラーにならないかと言うと static 関数だからだ。staic 関数なので、各々の .c や .cpp を越えてリンカーが参照させることは無い。そのため、シンボルの重複が起きないのだ。ヘッダファイルの内容と .c や .cpp ファイルの内容はプリプロセッサに処理されてからコンパイラに渡される。そのため、コンパイラが見るのは大きなファイルが一つだけ。プリプロセッサにどのファイルからどこの行が読まれたかの情報は組み込まれているが、それらを元に、static 関数がヘッダーファイルにあるのを警告するコンパイラは、知る限りでは無い。

static 関数や static 変数をヘッダファイルに見付けたら、即刻修正の対象だ。厄介なことに、コンパイラやリンカーはこの問題を検出しない。また、間違ってはいても、問題の無い動作をすることが多いのも特徴だ。具体的な問題としては、コンパイルに掛かる時間が増える。リンカーも時間が掛かる。実行形式のファイルの大きさが増える。実行時のメモリの利用量も増えるなど多岐にわたる。

Purify でプログラムが起動しない2015年08月04日 11時10分12秒

Purify はオブジェクトファイルに検査の為のコードを埋め込み、実行時の各種問題の検査をしてくれる。プログラムのリンクの時間とプログラムの実行時間がとてつもなく遅いのが珠に傷なのだが、その検知力は他を追随しない。ゆっくりと時間を掛けてプログラムを生成し、時間に余裕をもってテストを行なう。

さて、久方に、Purify を使った。しかし、Purify を組み込んだプログラムが起動しない。エラーメッセージがいかの様に出る。

the two libraries might interfere with each other, leading to unpredictable results. This happens occasionally when you share the cache between multiple machines running the same version of the OS.
要約すると、「プログラムが同名の複数の実装のライブラリを組み込んでいる。」となる。

Purify を用いないとプログラムは起動する。Purify だと駄目。

ldd などを用いて詳しく調べると、libm が SunStudio とシステムの両方に参照されていた。プログラムは起動こそするものの、どちらの実装が使われるのかはそれこそ運次第。libm は数学系のライブラリなので枯れてはいるので、おそらく問題が表面化しないのだろう。

もし上記のエラーで困ったのだったら、-L にて指定されているライブラリのパスを調べたら良いだろう。

Solaris の fatal: relocation error:2013年10月04日 13時56分24秒

Solaris でプログラムを実行時に以下のエラーを残して急に落ちることがある。

ld.so.1: XXX fatal: relocation error: XXX: XXX: referenced symbol not found

XXX の部分は見付からなかった関数名だ。これが起こる原因はおそらくこの二つ。

まずは、リンク時と実行時のライブラリに不整合がある時になる。リンクされたバイナリを他のホストに移したが、同じライブラリが無かった場合に起こる。良く起こる場合としては、他所から共有ライブラリを使っているが、うっかり全てのホストにはインストールされていなかったなど。

そしてもう一つは、-lznodefs をリンカに渡して、解決仕切れなかったシンボルが存在したときに、起きる可能性がある。なぜ、起きる可能性があるかと遠回しな言い方をするかというと、実行時にこの関数を呼ばなければ、落ちることは無いからだ。

-lznodefs は名前から察する通り、解決仕切れないシンボルがあっても、プログラムのリンクを正しく終了できた事にしてしまうオプションだ。この事からも、本番環境に用いるプログラムでは、絶対に避けるべきオプションである。このオプションを使わないと、一つでも見付からないシンボルがあるとプログラムの生成は成功しない。

それではいつ使うかと言うと、やはり開発環境になる。共有ライブラリを用いたり、相互依存の醜いライブラリ群があると、いちいち再リンクをするのに時間と手間がかかる。開発環境で部分的な試験をしている時は、全部のコードが実行される事はほとんど無いので、業と呼ばれない部分を無視してしまうのだ。どうせ、本番環境では使わない邪道を用いて開発効率を上げているのだから、いちいち関係の無い細部に拘る必要もない。

しかし、時として見付からないシンボルがあって、実行時に落ちることがある。そんなときは、ldd -d を使って、どのシンボルが解決できていないかを探る。

-qlinedebug はあまり役にたたない2013年09月10日 12時31分51秒

AIX のコンパイラには -qlinedebug というオプションがある。デバッグオプションの -g とは別途用いる。

IBM のサイトをみると以下のように説明されている。

目的 デバッガー用に行番号およびソース・ファイル名の情報のみを生成する。 -qlinedebug が有効な場合、コンパイラーは最小限のデバッグ情報しか生成しないため、 結果として得られるオブジェクト・サイズは、 -g デバッグ・オプションを指定した場合に生成されるオブジェクトよりも 小さくなります。 デバッガーを使用してソース・コードをステップスルーすることができますが、変数情報を表示したり照会することはできません。 トレースバック・テーブルを生成させると、 行番号が組み込まれます。

つまり、デバッガで各々の行を実行できるが変数は見ることは出来ない。コードにも依るのだが、-qlinedebug-g に比べると、一割から二割程度のバイナリサイズを削減できる。なお、strip を使うと、七割から九割ぐらいのぜい肉をそぎ落とすことが出来る。実行ファイルのほとんどは余分な情報なのだ。

core ファイルを開くと、最後のトレースは見られるが変数が一切見られないので、確かに落ちた辺りは見られるが、既に痕跡は無くなっているのだ。core を調べる時に役に立つのがやはり、変数の値。最適化によって、直には見られなくても、大きな手がかりになるので、これが無くなってしまうのは痛い。

AIX では strip された実行ファイルからの core であっても、strip する前の実行ファイルを使えるらしい。もし、それが真実だとすると、linedebug を使うよりも strip した方が core は見やすいことになる。まだ、自身での経験はあまりないので、もっと見極める必要がある。

AIX のリンカーと動的ローダの癖2013年09月06日 12時27分41秒

AIX のリンカーと動的ローダは、機能的には Solaris のより上だと思うが、その分だけ癖があり、若干厄介な面もある。

具体的な利点としては、ライブラリをリンクする時の Solaris のリンクの単位がファイル毎名のに対し、AIX ではシンボル単位なのだ。Solaris は呼ばれない関数が一つのファイルにあったら、否応なしに全てが取り込まれる。AIX では必要な関数だけだ。

AIX では呼ばない関数をリンクしないので Solaris に比べて実行ファイルが大きくなりにくい。動的ライブラリも併せて行なったときに、その呼ばない関数の切り落としをやりすぎてしまうみたいだ。

AIX では実行ファイルをロードしている時に全てのシンボルを読み込もうとする。もし、プログラムの中に呼ばれないが、外部参照されている関数などがあると、実行できなくなる。リンクは出来るのに起動できないのだ。

リンク時に -blazy を渡すとこれらのシンボルも探すようになる。

-xs を用いずにデバッグシンボルを組み込むのを抑制する2013年08月04日 11時20分32秒

動作するプログラムを書くことを出来る人は大勢いるが、正しくコンパイルする事、効率的にコンパイル、リンクをする事が出来る人はほとんどいない。大部分の人達が、自分達が毎日、自分で何を行なっているかを把握していないのだ。知らず知らずのうちに、無駄を毎日の様に繰り返していることもしばしば。少しの変更で、大きな無駄を省けることも多々ある。一人で一割、二割の時間を有効に使えるようになるとしよう。それが、百人に及んだら事は重大だ。

Solaris には -xs というコンパイルオプションがある。これによりオブジェクトファイルなしに dbx でデバッグできるようになる。これを用いると、各々のオブジェクトファイルに入っているデバッグ情報が、実行ファイルへ全てコピーされるのだ。これが、リンクオプションで無いのは若干の不思議。

さて、このオプションを有効にするには -xs を指定する必要がある。これは、コードや中間生成物を共に移さない、本番系に有効なオプションだ。

つまり、このオプションを利用しないと、プログラムのサイズが大幅に小さくなる。元の一割やそれ以下の大きさにまでなってしまった等というのは日常茶飯事だ。

これを省くと、プログラムのリンクの時間が大幅に減る。デバッガの起動と再読み込みの時間も大きく減る。そして、ディスクの圧迫も回避となる。

もちろん、これを使わない不利益は、オブジェクトファイルが必要なこと。何かあった時に心配との事で、常に指定している組織も多い。

逆にいえば、一般的な開発環境では必要無い。そして、本番環境にオブジェクトファイルが無くても、core ファイルを持ってくれば、しっかりとデバッグが出来る。つまり、コードとオブジェクトファイルとしっかりと保持する運用体勢を確立していれば、用いなくても問題はない。または、本番用にのみ指定する運用でもいい。

成果物からは、弘法筆を選ばずの世界なのだが、その過程には、過ぎたるは猶及ばざるが如し、そして、労多くして功少なしの世界でもあるのだ。

なお、-xs は -xdebugformat=stabs を指定し、stabs 形式のデバッグ情報を用いる時に有効になる。-xdebugformat=dwarf で古い型の dwarf 形式を用いている場合には無意味だ。

AIX はある筋の情報によると stabs で、実行ファイル外にデバッグ情報を置くことは出来るが、dbx がオブジェクトファイルからその情報を読むことがまだ出来ない為、似たようなオプションは無いそうだ。

strtok で最初の一語を取得2013年02月06日 14時09分43秒

何か変わったことをやっているコードだとは思っていたが、長いこと何をやっているのか分らなかった。少し簡単にした例を上げる。

% cat sentence.c
#include <string.h>
#include <stdio.h>

int main()
{
    char sentence[] = "This is a pen";
    strtok( sentence, " " );
    puts( sentence );
}
% CC sentence.c          
% ./a.out 
This

strtok は基本的に文字列を区切りたいときに使い、その戻り値を使うのが定石。そんなに使いやすい関数ではないが、C ライブラリにも長年あるので、ちょっと使いたいときに便利。

sentence を表示しているが、strtok の副作用で、最初の空白で切れるために、最初の単語のみを切り出すことになる。

分りづらかった理由の一つが、戻り値を使わないこと。なぜ、唐突に strtok と不思議に思っていた。

そして、その分りづらさに更に一捻り加えたのがその入力値。ほぼ全ての値が元々、一単語なのだ。今まで見てきた全ての場合が一単語。ちょっと変わった場合を試して、初めて二単語のケースに遭遇した。その結果を見てやっとその意図が汲めた。

関数を本来の目的とは異なった事に用いて、その副作用だけを活用する、とても悪い関数の使い方の例。

ファイルデスクリプタを fileno で取得。2012年10月12日 12時06分47秒

stdio.h に int fileno(File *file) という関数がある。fopen() で開いたファイルから、open() で開いた時に渡されるファイルデスクリプタを取得できる。

FILE 構造体からは、fwrite や fread を使わなければ、読み書きできない。read や write で直接入出力を行なう時には、FILE から int を取り出す必要がある。また、fstat や fchown など、ファイルデスクリプタから操作できるライブラリ関数もけっこうある。

三回続けて名前が出てこなかったのでメモ。

open でモードを設定し忘れたら、読み書きできなかった2012年05月14日 11時50分52秒

ちょっとした失敗。open システムコールを使って、ファイルを作成したが、パーミッションを指定するのを忘れていた。そうしたら、--S になっていて、読み書きができない。

open システムコールは、引数が可変で、二つ又は三つの引数を取る。三つ目の引数は、ファイルを作成したときのパーミッションで、二つ目の引数に O_CREAT が入っている時に有効になる。


% cat open.c 
#include <fcntl.h>

int main(int argc, char* argv[])
{
    open(argv[1], O_CREAT | O_WRONLY | O_TRUNC);
}

ファイルが存在しない時の open は、ファイルを作成して、ファイルディスクリプタを返す。書き込み様に開いているので、最初に実験したときは、問題なく書き出せるのだ。

しかし、落とし穴は二回目と読み込み時にある。--S のパーミッションになってしまい、自分で読み書きできない。最初に動かした時は、書いた内容。内容どころか書かれているのをファイルの大きさを見て点検しただけだったので、アクセス権限は全く見逃していた。

そんなこんなで、ファイルの読み書きが失敗しているのに気が付いてみたら、open の仕業。


% cat open.c 
#include <fcntl.h>

int main(int argc, char* argv[])
{
    mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
    open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, mode);
}

これで、自分は読み書き。そして、それ以外は読める。

exit を複数回呼ぶ2012年04月02日 19時51分10秒

exit は C 言語のライブラリ関数。stdlib.h に入っている。

実はこの関数を二回以上呼んだときの動作は不定になっている。実質のところ、運が良ければ何も怒らない。何か起きるとしたらセグメンテーション違反になる。

大雑把に分けると、問題が起きるのマルチスレッドの C++ 環境。C 言語や、シングルスレッドの C++ では起きない。

そして、セグメンテーション違反の理由は既に破棄されたオブジェクトを参照することによって起きる。exit が呼ばれると、プログラムは終了の準備に入る。各種リソースの解放だ。その中の一つに、静的オブジェクトの解放だ。static で広域に割り当てられているオブジェクトが次々に破棄されていく。これを再度参照することによって、すでに解放されているメモリ領域に足を踏み入れることになる。

純粋仮想関数を使っていると、関数の実装が見付からないなどといった、滅多に見ることの無い実行時エラーを見ることも出来る。

シングルスレッドの場合は、exit を呼んだ時点で制御を渡してしまうので、そもそも新たに何かをすることは出来ない。C 言語ではデストラクタが無いので、基本的に起きづらい。

マルチスレッドで、慎重を期したつもりで複数回呼べば大丈夫だと思ったのだろう。まずは、動作も良く判っていない関数の動作と規定を慎重に調べるところから始めた方がいい。