5/30 (金)
[Prog] 動的リンカが勝手にレジスタを破壊するよ…
ここ数日間はまっていたバグの覚え書き。 日記に書き留めて教訓としよう。
IA-64 のアセンブラで tail-call な関数呼び出しを行う処理を書いたのだが、 アセンブラソースファイルを -fPIC をつけてアセンブルすると br.call でない br 命令でもリロケータブルになる。
.global TAIL_CALL_FUNC
.type TAIL_CALL_FUNC, @function
.proc TAIL_CALL_FUNC
FUNCTION_BODY::
br.sptk.many TAIL_CALL_FUNC;;
TAIL_CALL_FUNC を含む br 命令のためにインポート・スタブが作成されるのだが、 動的ローダが初回に動作する際にこちらが使用している出力レジスタを破壊していくようだ。 .o ファイルをディスアセンブルしてもレジスタを破壊しているようなコードが見つからず、 実質丸一日潰れてしまった。
TAIL_CALL_FUNC に .hidden TAIL_CALL_FUNC
のように visibility を指定してやると、
レジスタ破壊は起きなくなる。
5/12 (月)
[CPU] TLB を積極的に使用する命令
ここ数日、
x86-64 アーキテクチャと IA-64 アーキテクチャを比較調査しているのだが、
IA-64 命令に存在する probe.{r,w}
命令や probe.{r,w,rw}.fault
命令を x86-64 でどうやって書けばいいのか悩んでいる。
IA-64 命令の場合、
probe.{r,w}
命令は指定したアドレス(仮想アドレス)に対応する TLB エントリの属性を見て、読み込み可能/書き込み可能を転写してくる。
probe.{r,w,rw}.fault
命令の方は指定したアドレス(仮想アドレス)に対応する TLB エントリの属性が読み込み(r)、書き込み(w)またはその両方(rw)を満たさなければ、対応した TLB フォルトを発行することになる。
Fault 付きでない probe.{r,w}
命令は
mprotect で書き込み保護が指定されたページをシステムコールを発行せずにチェックできるし、
Fault 付きの probe.{r,w,rw}.fault
命令は
実際の書き込みを行わずに書き込み保護違反を起こすことが可能だ。
IA-64 で書いていたプログラムを x86-64 に移植する場合、 probe 系命令をエミュレーションする手段が欲しいのだが 同様の命令が x86-64 の命令セット中にはない(VERR/VERW は似ているが probe 命令の替りにはならない)。 なんか面倒なことになりそう。
IA-64 の probe
命令のような TLB を積極的に使う命令は、
論文の中で最近よく見るようになったし
Azul の専用CPUなど
本当に実装例も存在する。
ただこの手も命令は既存の CPU ではエミュレーションし難いために、
移植が大変ナリ。
もう遅いけど…
追記:2008/12/31
いくつか代替方法を検討。
- いったんメモリから値を読み込んで、 メモリ値と同じ値を cmpxchgb 命令で書き込む。
- LOCK プレフィックスを付けた add 命令でメモリ上の値に 0 を足す。
5/8 (木)
[CPU][Linux] AR.BSP レジスタが指す先
IA-64 は汎用レジスタが 128 本あるが、 r32 から r127 までの 96 本はスタック汎用レジスタと呼ばれ、 関数呼び出しの・復帰にあわせて自動的に回転するようになっている。 物理レジスタから溢れたスタック汎用レジスタは レジスタ・スタックと呼ばれるメモリ領域に書き出されるのだが、 その時 r32 番にあたるレジスタの退避先になる領域をしめしているのが ar.bsp レジスタだ。
そのためレジスタコンテキストをスタックフレームの外側から書き換えたい場合には、 ar.bsp の位置を探すことが重要になる。
ところが IA-64/Linux で以下のプログラムを実行すると、
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <ucontext.h> #define UNW_LOCAL_ONLY #include <libunwind.h> static void signal_handler(int sig, siginfo_t* sig_info, struct ucontext* ucp) { printf("\n[libunwind in signal]\n"); printf("ucp->uc_mcontext.sc_ar_bsp = %lx\n", ucp->uc_mcontext.sc_ar_bsp); printf("ucp->uc_mcontext.sc_cfm & 0x7F = %lx\n", ucp->uc_mcontext.sc_cfm & 0x7FUL); unw_cursor_t cursor; unw_context_t uc; unw_getcontext(&uc); unw_init_local(&cursor, &uc); while (unw_step(&cursor) > 0) { unw_word_t ip, bsp; unw_get_reg(&cursor, UNW_REG_IP, &ip); unw_get_reg(&cursor, UNW_IA64_AR_BSP, &bsp); printf ("ip = %lx, bsp = %lx\n", (long) ip, (long) bsp); } exit(0); } int main() { struct sigaction act; sigemptyset(&act.sa_mask); act.sa_sigaction = signal_handler; act.sa_flags = SA_SIGINFO | SA_RESTART; if (sigaction(SIGSEGV, &act, NULL)) { perror("sigaction error"), exit(1); } // ダーティーなスタック汎用レジスタを全てレジスタスタックに書き出し __asm__ __volatile__("flushrs"); // 本当の bsp を求める uint64_t res; __asm__ __volatile__("mov %0=ar.bsp" : "=r"(res) :: "memory"); printf("[True]\nbsp = %lx\n", res); // unwind ライブラリ { unw_cursor_t cursor; unw_context_t uc; unw_getcontext(&uc); unw_init_local(&cursor, &uc); printf("\n[libunwind in main]\n"); while (unw_step(&cursor) > 0) { unw_word_t ip, bsp; unw_get_reg(&cursor, UNW_REG_IP, &ip); unw_get_reg(&cursor, UNW_IA64_AR_BSP, &bsp); printf ("ip = %lx, bsp = %lx\n", (long) ip, (long) bsp); } } // getcontext を使う { ucontext_t uc; getcontext(&uc); printf("\n[getcontext]\n"); printf("bsp = %lx\n", uc.uc_mcontext.sc_ar_bsp); printf("cfm & 7F = %lx\n", uc.uc_mcontext.sc_cfm & 0x7FUL); } // SIGSEGV シグナルを作る *(int*)0 = 1; return 0; }
その実行結果は以下のようになる。
[True] bsp = 60000fff7fffc050 [libunwind in main] ip = 2000000000131430, bsp = 60000fff7fffc050 ip = 4000000000000a80, bsp = 60000fff7fffc050 [getcontext] bsp = 60000fff7fffc070 cfm & 7F = 0 [libunwind in signal] ucp->uc_mcontext.sc_ar_bsp = 60000fff7fffc088 ucp->uc_mcontext.sc_cfm & 0x7F = 7 ip = a0000000000107e0, bsp = 60000fff7fffc088 ip = 4000000000001140, bsp = 60000fff7fffc088 ip = 2000000000131430, bsp = 60000fff7fffc088 ip = 4000000000000a80, bsp = 60000fff7fffc088
結果をまとめると以下のようになる。
- 真の ar.bsp は 0x60000fff7fffc050 で、 同じ関数内で libunwind で得た bsp の値は一致する。
- 同期シグナルをわざと作りシグナルハンドラ側で
struct ucontext
へのポインタからucp->uc_mcontext.sc_ar_bsp
を調べると、 0x60000fff7fffc088 で真の値よりも 56 バイトほど大きな値がえられる。
これは IA-64/Linux がカーネル内でユーザスレッドの bsp を退避する前にcover
命令を発行して、 現在のカレントフレームの sof (stack of frame) をレジスタスタック側に繰り込んだ後の値になっている。ucp->uc_mcontext.sc_ar_cfm
の下位 7 ビットに sof が記録されているので、(sc_ar_bsp) - (sc_ar_cfm & 0x7F) - ε
を計算すると真の ar.bsp に一致する。
ε
はレジスタスタックに 63 本レジスタを退避するごとに余分に保存する必要のある NaT コレクションによる調整分で、ε = ((((sc_ar_bsp / 8) & 0x3F) - (sc_ar_cfm & 0x7F) - 0x3E) / 0x3F)
となる。 - 同期シグナルハンドラ内で libunwind を使って得た bsp の値は、
ucp->uc_mcontext.sc_ar_bsp
と一致して真の値にならない。 - getcontext 関数を使った結果の bsp 値は、真の値よりも 32 バイト大きい。 この理由は不明。
あまり統一感がないのが困り者。む〜。
5/7 (水)
日経コンピュータ 5/1 号
緊急特集でやっているスルガ銀行×日本IBMの訴訟が気になって購入して読んでみる。
記事はスルガ銀行側の言い分にもとづいて記述されているので、 日本IBM側に非があるように読める。 2004年9月着手・2008年1月システム稼動予定の開発計画なのに、 2007年4月の段階で 日本IBMから「NEFSS/Corebank の採用を断念し、別の製品に切り替えて欲しい」といわれれば、 スルガ銀行も切れますなぁ…
5/3 (土)
[Prog] abort 関数の動作の違い
SIGABRT シグナルハンドラの中で abort
関数を呼び出すプログラムを書いた場合、
OS によって動作が変わってくることに気づいた。
Solaris と HP-UX ではシグナルハンドラ内の abort
関数を呼び出すとその時点でコアダンプを開始する。
一方、Linux の場合には再帰的に SIGABRT シグナルへ飛んでしまい、
スタックがオーバーフローするまで止まらない。
この違いは Linux の abort
関数の実装方法に起因しているようだ。
Linux の GLIBC はシグナルマスクによって SIGABRT シグナルがブロックされている場合、まずそれを解除してから SIGABRT シグナルをあげることになっている。
SIGABRT シグナルハンドラは SIGABRT シグナルをブロックしているが、
これが前述の効果でブロックが解除されるようだ。
逆に Solaris や HP-UX では SIGABRT シグナルの解除を行わないので、
SIGABRT シグナルハンドラ内で abort
関数を呼ぶと
OS 規定の処理(つまりコアダンプ)が開始される。
Linux の abort
の実装は、
SIGABRT シグナルハンドラ内でファットなエラーハンドル処理をする場合に都合が悪い。
エラー情報の収集のためにいろいろな関数を呼び出すと、その中で abort
が使われている可能性があるからだ。
これを避けるには、
SIGABRT シグナルハンドラの先頭で SIGABRT シグナルのアクションをデフォルトに戻す必要がある。
5/2 (金)
凄いアスキーアート
なんか凄い
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
□□囗囗□□□□□□□□□□□□□□□□□□□□□□□□□□□
□□□□囗囗□□□□□□□□囗□□□□□□□□□□□□□□□□
□□□□□□□□□□□□□囗□□□□□□□□囗囗囗囗囗囗囗□□
□□□囗囗囗囗□□□□□□囗□□□□□□□□□□□□□□□□□
□囗囗□□□□囗□□□□囗□□□□□□□□□□□□□□□□□□
□□□□□□□囗□□□□囗□□□□□□□□□□□□□□□□□□
□□□□□□□囗□□□囗囗囗囗□□□□□□□□□□□□□□□□
□□□□□□□囗□□□囗□□□囗□□□囗□囗□□□□□□□□□
□□□□□□囗□□□囗□□□□囗□□□囗□囗□□□□□□□□□
□□□□□囗□□□□囗□□□□囗□□囗□□囗□□□□□□□□□
□□囗囗囗□□□□囗□□□□□□囗囗□□□□囗囗囗囗囗囗囗囗□
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
上の四角の集合体を凝視しながら
マウスのホイールを使って小刻みに上下にスクロールしてみよう