目次: GCC
GCCには自動ベクトル化(tree-vectorize)機能があります。ループ処理を自動的にSIMD命令に置き換えるために使われているようです。現状のGCCが可変長のベクトル長に対応しているかどうかはわかりません。未対応ならば可変長のベクトル長に対応する実装が必要になりますが、非常に難しそうです。
可変長のベクトルの扱いはひとまず横に置くとして、RISC-Vのベクトルを「とても長い固定長のSIMD」とみなして自動ベクトル化を動かします。
// gcc/config/riscv/riscv.c
/* Implement TARGET_VECTORIZE_AUTOVECTORIZE_VECTOR_MODES. */
static unsigned int
riscv_autovectorize_vector_modes (vector_modes *modes, bool)
{
if (TARGET_VECTOR)
{
modes->safe_push (V64SImode);
modes->safe_push (V32SImode);
}
return 0;
}
...
#undef TARGET_VECTORIZE_AUTOVECTORIZE_VECTOR_MODES
#define TARGET_VECTORIZE_AUTOVECTORIZE_VECTOR_MODES riscv_autovectorize_vector_modes
自動ベクトル化を有効にする方法は簡単で、これだけです。
void *cpy(void *dst, const void *src, int n)
{
int *d = dst;
const int *s = src;
int i;
for (i = 0; i < n / sizeof(*d); i++) {
d[i] = s[i];
}
return dst;
}
この関数がベクトル化あり、なしでどのように変わるか見ます。
//★★自動ベクトル化、あり
d[i] = s[i];
100b8: 1202e007 vlw.v v0,(t0)
100bc: 10038393 addi t2,t2,256
100c0: f0038793 addi a5,t2,-256
100c4: 10028293 addi t0,t0,256
100c8: 0207e027 vsw.v v0,(a5)
for (i = 0; i < n / sizeof(*d); i++) {
100cc: fee296e3 bne t0,a4,100b8 <cpy+0x44>
100d0: 00661293 slli t0,a2,0x6
100d4: 02568963 beq a3,t0,10106 <cpy+0x92>
100d8: 959a add a1,a1,t1
100da: 932a add t1,t1,a0
d[i] = s[i];
100dc: 0005a383 lw t2,0(a1)
//★★自動ベクトル化、なし
d[i] = s[i];
10080: 0005a303 lw t1,0(a1)
for (i = 0; i < n / sizeof(*d); i++) {
10084: 0591 addi a1,a1,4
10086: 0291 addi t0,t0,4
d[i] = s[i];
10088: fe62ae23 sw t1,-4(t0)
for (i = 0; i < n / sizeof(*d); i++) {
1008c: fe759ae3 bne a1,t2,10080 <cpy+0xc>
}
return dst;
}
10090: 8082 ret
ソースコードではベクトル型を使っていませんが、自動ベクトル化により256バイト(=64要素)ずつ処理され、vlw.v, vsw.v命令が使われるようになったことがわかります。
目次: OpenCL
OpenCLは複数のベンダーのデバイスを同時に扱うことができます。ICD(Installable Client Driver)というそうです。ICDはGPUなどのデバイスを制御し、アプリケーションとICDの間にICDローダーが存在します。
Debian TestingではICDローダーとしてocl-icd-2.2.12が使われています。
$ apt-cache search ocl-icd-libopencl1 ocl-icd-libopencl1 - Generic OpenCL ICD Loader
ローダーのソースコードはGitHub(リンク)にあります。
先程、図示したもの以外にもICDはいくつか実装があります。現時点のDebian Testingでは下記が提供されていました。
IntelはICDのソースコードを完全にオープンにしています。NVIDIAは公開していません。AMDもないのかな?
動作を追ってみたいと思います。アプリケーションにはclinfoを使います。ocl-icdは環境変数OCL_ICD_DEBUG=15に設定すると、動作時に詳細なログを出力します。デバッガで追うのと併用するとわかりやすいです。
ローダーが走査するディレクトリは /etc/OpenCL/vendorsがハードコードされていますが、環境変数OPENCL_VENDOR_PATHで変更できます。
ICDのロードの中心となる処理は _find_and_check_platforms() です。
プラットフォームは説明が難しいですが、OpenCL APIの実体+任意のドライバ固有のデータとでも言いましょうか。変数の型はcl_platform_id * 型です。cl_platform_idは少なくとも先頭のメンバはstruct _cl_icd_dispatch *dispatchでなければなりません。dispatchの後ろには他の情報が入っていても問題ないようです。
// ocl-icd/ocl_icd_loader.c
static inline void _find_and_check_platforms(cl_uint num_icds) {
cl_uint i;
...
cl_platform_id *platforms = (cl_platform_id *) malloc( sizeof(cl_platform_id) * num_platforms);
error = (*plt_fn_ptr)(num_platforms, platforms, NULL);
...
for(j=0; j<num_platforms; j++) {
debug(D_LOG, "Checking platform %i", j);
struct platform_icd *p=&_picds[_num_picds];
char *param_value=NULL;
p->extension_suffix=NULL;
p->vicd=&_icds[i];
p->pid=platforms[j]; //★★pid = platform IDのことらしい
/* If clGetPlatformInfo is not exported and we are here, it
* means that OCL_ICD_ASSUME_ICD_EXTENSION. Si we try to take it
* from the dispatch * table. If that fails too, we have to
* bail.
*/
if (plt_info_ptr == NULL) {
plt_info_ptr = p->pid->dispatch->clGetPlatformInfo; //★★dispatchメンバが存在することを前提としている
// ocl-icd/khronos-headers/CL/cl.h
typedef struct _cl_platform_id * cl_platform_id;
// ocl-icd/(build-dir)/ocl_icd_loader_gen.h
struct _cl_platform_id { struct _cl_icd_dispatch *dispatch; }; //★★dispatch以外は特に規定がなさそう
// ocl-icd/(build-dir)/ocl_icd.h
struct _cl_icd_dispatch {
#ifdef CL_VERSION_1_0
CL_API_ENTRY cl_int (CL_API_CALL*clGetPlatformIDs)(
cl_uint /* num_entries */,
cl_platform_id * /* platforms */,
cl_uint * /* num_platforms */
) CL_API_SUFFIX__VERSION_1_0;
#else
CL_API_ENTRY cl_int (CL_API_CALL* clUnknown0)(void);
#endif
#ifdef CL_VERSION_1_0
CL_API_ENTRY cl_int (CL_API_CALL*
clGetPlatformInfo)(
cl_platform_id /* platform */,
cl_platform_info /* param_name */,
size_t /* param_value_size */,
void * /* param_value */,
size_t * /* param_value_size_ret */
) CL_API_SUFFIX__VERSION_1_0;
#else
CL_API_ENTRY cl_int (CL_API_CALL* clUnknown1)(void);
#endif
...
//★★こんな調子で関数ポインタの定義が延々と続く
先頭のdispatchはたくさんの関数ポインタが並んだ巨大な構造体です。アプリケーションから見るとOpenCLのAPIはocl-icdが提供しているように見えますが、ocl-icdのAPI実装はdispatchの関数ポインタを呼ぶラッパー関数であり、関数ポインタと、OpenCL API実装の本体を提供するのは各ICDの役割です。
目次: ゲームに統合。
目次: ゲーム
MAD Tower Tycoonでゾーン方式のエレベータの作り方を悩んで色々やっていたら、いつのまにかレベル100、実績コンプリートしていました。ゲーム内時間は表示されないので詳しくはわかりませんが、おそらく700日くらい?
The Towerと比べて色々思うところはありますが、総合的にみれば面白いと思います。
序盤だけ金欠になりやすいですが、難易度はかなり低めですし、ビル建築シミュレーション初めての方にオススメしたいゲームです。
MAD Tower Tycoonは住人を追尾する機能があり、ビルの住人が困っていないか把握するのに便利です。基本的には目的地に行って、家に帰る(住人の場合)、もしくはビルから出る(ゲストの場合)だけですけど、大きなビルを作ると変な行動が目立ちます。
前回(2020年7月10日の日記参照)も書きましたが、MAD Tower Tycoonはゾーン方式のエレベーターを作る方法がわかりません。今はメンテナンス施設の射程(上下6Fに効果がある)の関係で、7階+8階(※)おきに乗り換え階を作っています。
どうもこの方式だと40階くらいで限界っぽいです。乗り換えに時間が掛かりすぎて、目的地に行くだけで半日費やしている気の毒な住民がいます。彼らはなぜか不満は言いませんが、見ていると不憫です……。
救いとしてはMAD Tower Tycoonはビルの横幅がめちゃくちゃ広く取れるので、45階もあれば、レベル100、五つ星ビルが余裕で作れることです。だけど、やっぱりThe Towerにあやかるなら、100階建て目指したいですよね?
(※)効率重視ならば7F, 14F乗り換えが最適ですが、スカイロビーを作成できるのは15Fからなので、あえて14Fを空きフロアにして、15F乗り換えにしています。
MAD Tower Tycoonは、The Towerとかなり似ているがゆえに、つい比較してしまいます。
良いところ
悪いところ
ゾーン方式のエレベーターが作れたら、高層ビルに効率的に人を運べるようになって、もっと面白くなるはずなのに。もったいないよ〜。
目次: ゲーム
STATIONflowの実績をコンプリートしました。「ラッキーセブン」と「東京」は自動化マクロを組まないと取れませんでした。
本来、このゲームにハマって色々なマップをずっと遊んでいたらいつのまにか取れていた、というタイプの実績ですが、申し訳ないことに、私はそこまでの情熱がなかったです。
ゲームやったらわかりますけど、この2つの実績だけ条件設定が異常すぎます。
実績の条件(総利用者数700万人、1,390万人)がいかに異常かがわかると思います。
自動化の方法は簡単で、PowerShellでEnterキーを3秒に1回送るマクロを組んで、会社行っている間や夜間に放置するだけです。2週間くらいで取れました。
STATIONflowは放置しても悪いイベントが起きない(=駅が壊れない)優しい仕様になっているため、自動化+放置が可能でしたが、他のシミュレーションゲームだと偶発的に悪いイベントが起きるため、この方法は使えません。
まあ、明らかに製作者の想定した取り方ではないし、こんな方法で実績取っても嬉しくないし、無理に実績取るのは今後はやめておきます。
目次: ゲーム
最近Steamで買った、MAD Tower Tycoon(開発: Eggcode)で遊んでいます。
ビル建築&経営シミュレーションゲームです。プレイヤーはビルを拡張していって、テナントや住居を作ります。テナントには住人やゲストが押し寄せてきますので、住人のストレスを溜めないように、メンテナンス施設を足したり、階段、エスカレーター、エレベーターでうまく捌き、儲けを出して、ビルをさらに拡張させるというゲームです。
ビル建築&経営シミュレーションの名作はThe Tower(開発: OPeNBooK)だと思います。The Towerは続編が出なくなって久しく寂しかったですが、MAD Tower TycoonはかなりThe Towerに近い作りです。
後追いだけあって基本的にThe Towerより良くなっていますが、残念な部分もあって、
難易度は最近の風潮で優しめだとしても、最後のエレベーターの劣化は残念です。
エレベーターはThe Towerのキモで、説明書でも詳しく説明していました。MAD Tower Tycoonのエレベーターの場合は、
特に後者の制限が厳しくて、ゾーン方式(※)エレベーターが実質建設不可能で、高層ビルを作るには厳しい仕様となっています。
(※)エレベーターを複数基用意して、1つ目は1〜5階のみ、2つ目は5〜10階のみなど、一部の階しか止まらないエレベーターを作る方式のことです。現実でも高層ビルでよく見かけます。
バグなのか仕様なのかわからないですが、MAD Tower Tycoonではバルコニーでフロアをぶち抜いて、スカイブリッジを作ると、どことも繋がらない孤立したフロアを作れます。
孤立したフロアにエレベーターだけを設置することで、実質的に乗車降車禁止階にできます。
ところがこの乗車降車禁止エレベータを作っても、なぜか住人は一切利用してくれません。訳が分かりません。どうしたら良いんでしょうか??
The Tower開発元のOPeNBooKは合併と名前変更を繰り返しています。合併相手は9003, incで、AQUAZONEという熱帯魚育成&水槽シミュレーションで名を馳せたベンダーです。
1993 2000 OPeNBooK ---, 1996 ,--> オープンブック(The Towerの版権を持つ) +--> オープンブック9003 ---+--> シノミクス 9003, inc ---' 1990
Wikipediaを見るとこんな経歴でした。お互い、元の鞘に収まったという感じがします。合併したけどやることがなかったんですかね?
目次: Windows
前回(2020年6月29日の日記参照)、ノートPCの発熱を抑えるためにCPUのクロック上限を抑える設定をしました。
前回はTurboBoostだけ無効化する方法がわかりませんでしたが、ググっていたら割と簡単にTurboBoostを無効化できることがわかりました。
レジストリエディタで、
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Power\PowerSettings\54533251-82be-4824-96c1-47b60b740d00\be337238-0d82-4146-a960-4f3749d470c7
のAttributesというDWORD値を1から0 or 2に変更します(※)。
すると下記の「プロセッサ パフォーマンスの向上モード」オプションが出現しますので、無効を選択します。
TurboBoostを無効化すると、最大のプロセッサの状態を100% にしても、ベース周波数の1.6GHzまでしか周波数が上がらなくなります。これは良い感じだ。
(※)私はSurface ProでCPUクロックが最大に張り付くという問題(フォーラムへのリンク)の解決策に載っていたものを見つけたのですが、どうも昔のWindows 8で消えた設定らしい(ASCII.jp : Windows 8.1で消えた詳細な電源管理項目を表示する!より)です。
目次: GCC
前回(2020年7月7日の日記参照)はベクトルレジスタが選択されてしまう仕組みが何となくわかりました。
今回やりたかったことを復習しておくと「ベクトルの演算以外でベクトルレジスタを使わないでほしい」でした。つまりira_prohibited_class_mode_regs[cl][j] のうちベクトル以外のmachine modeかつベクトルレジスタに相当するビットを「セット」つまり割り当て禁止状態にすれば良いはずです。
配列の次元のうちclはレジスタのクラス(enum reg_class)で、jはmachine modeです。レジスタのクラスは以前(2020年3月28日の日記参照)ちょっとだけ使いました。幸いなことに、今回はレジスタのクラスは気にしなくて良いです、というかコードのif文の条件hard_regno_mode_ok(hard_regno, (machine_mode) j) を見るとわかるように、そもそもレジスタのクラスが渡されないので、ターゲット側(=RISC-V依存の実装部分)で何もできないです。
重要なのはmachine modeで、ベクトル以外のモード(浮動小数点など)だったらベクトルレジスタを割り当て禁止状態にすれば良いです。
// gcc/config/riscv/riscv.c
/* Implement TARGET_HARD_REGNO_MODE_OK. */
static bool
riscv_hard_regno_mode_ok (unsigned int regno, machine_mode mode)
{
unsigned int nregs = riscv_hard_regno_nregs (regno, mode);
if (GP_REG_P (regno))
{
if (!GP_REG_P (regno + nregs - 1))
return false;
}
else if (FP_REG_P (regno))
{
if (!FP_REG_P (regno + nregs - 1))
return false;
if (GET_MODE_CLASS (mode) != MODE_FLOAT
&& GET_MODE_CLASS (mode) != MODE_COMPLEX_FLOAT)
return false;
/* Only use callee-saved registers if a potential callee is guaranteed
to spill the requisite width. */
if (GET_MODE_UNIT_SIZE (mode) > UNITS_PER_FP_REG
|| (!call_used_or_fixed_reg_p (regno)
&& GET_MODE_UNIT_SIZE (mode) > UNITS_PER_FP_ARG))
return false;
}
else if (VP_REG_P (regno)) //★★前回足した実装
{
return true;
}
else
return false;
...
ここでベクトル系のmachine modeを直接記述(mode == V64SImodeなど)しても間違いではないと思うのですが、単純にモードがたくさんあると鬱陶しいですし、該当するモードがあとで増えたときの修正が大変です。こういうときはmachine modeのクラスが便利です。クラスってなんだったかというと、machmode.defやriscv-modes.defに書いたあれです。
// gcc/machmode.def
...
/* Basic integer modes. We go up to TI in generic code (128 bits).
TImode is needed here because the some front ends now genericly
support __int128. If the front ends decide to generically support
larger types, then corresponding modes must be added here. The
name OI is reserved for a 256-bit type (needed by some back ends).
*/
//★★MODE_INTクラスになる
INT_MODE (QI, 1);
INT_MODE (HI, 2);
INT_MODE (SI, 4);
INT_MODE (DI, 8);
INT_MODE (TI, 16);
...
// gcc/config/riscv/riscv-modes.def
//★★MODE_FLOATクラスになる
FLOAT_MODE (TF, 16, ieee_quad_format);
//★★以前、追加した実装
//★★VECTOR_MODEの場合は少し特殊で、MODE_VECTOR + 最初の引数 クラスになる
//★★この例だとMODE_VECTOR_INTになる
VECTOR_MODE (INT, SI, 32);
VECTOR_MODE (INT, SI, 64);
クラスは上記の通り各所の *.defにて定義されますが、正直言ってどこにあるかわかりにくいし、クラスの名前も見えません。machine modeとクラスの対応を確認するだけなら、ビルド時に生成されるinsn-modes.cを見たほうが早いです。
// build_gcc/insn-modes.c
const unsigned char mode_class[NUM_MACHINE_MODES] =
{
MODE_RANDOM, /* VOID */
MODE_RANDOM, /* BLK */
MODE_CC, /* CC */
MODE_INT, /* BI */
MODE_INT, /* QI */
MODE_INT, /* HI */
MODE_INT, /* SI */
MODE_INT, /* DI */
MODE_INT, /* TI */
MODE_FRACT, /* QQ */
MODE_FRACT, /* HQ */
MODE_FRACT, /* SQ */
MODE_FRACT, /* DQ */
MODE_FRACT, /* TQ */
MODE_UFRACT, /* UQQ */
MODE_UFRACT, /* UHQ */
MODE_UFRACT, /* USQ */
MODE_UFRACT, /* UDQ */
MODE_UFRACT, /* UTQ */
MODE_ACCUM, /* HA */
MODE_ACCUM, /* SA */
MODE_ACCUM, /* DA */
MODE_ACCUM, /* TA */
MODE_UACCUM, /* UHA */
MODE_UACCUM, /* USA */
MODE_UACCUM, /* UDA */
MODE_UACCUM, /* UTA */
MODE_FLOAT, /* SF */
MODE_FLOAT, /* DF */
MODE_FLOAT, /* TF */
...
MODE_COMPLEX_FLOAT, /* SC */
MODE_COMPLEX_FLOAT, /* DC */
MODE_COMPLEX_FLOAT, /* TC */
MODE_VECTOR_INT, /* V32SI */
MODE_VECTOR_INT, /* V64SI */
};
ベクトル系のmachine modeを引っ掛けるにはMODE_VECTOR_INTを使えば良さそうです。今の実装では使っていませんがRISC-Vのベクトルは浮動小数点のベクトル(MODE_VECTOR_FLOAT)も扱えるはずなので、これも条件に追加しておきましょう。
// gcc/config/riscv/riscv.c
/* Implement TARGET_HARD_REGNO_MODE_OK. */
static bool
riscv_hard_regno_mode_ok (unsigned int regno, machine_mode mode)
{
unsigned int nregs = riscv_hard_regno_nregs (regno, mode);
if (GP_REG_P (regno))
{
if (!GP_REG_P (regno + nregs - 1))
return false;
}
else if (FP_REG_P (regno))
{
if (!FP_REG_P (regno + nregs - 1))
return false;
if (GET_MODE_CLASS (mode) != MODE_FLOAT
&& GET_MODE_CLASS (mode) != MODE_COMPLEX_FLOAT)
return false;
/* Only use callee-saved registers if a potential callee is guaranteed
to spill the requisite width. */
if (GET_MODE_UNIT_SIZE (mode) > UNITS_PER_FP_REG
|| (!call_used_or_fixed_reg_p (regno)
&& GET_MODE_UNIT_SIZE (mode) > UNITS_PER_FP_ARG))
return false;
}
else if (VP_REG_P (regno)) //★★前回足した実装
{
if (GET_MODE_CLASS (mode) != MODE_VECTOR_INT
&& GET_MODE_CLASS (mode) != MODE_VECTOR_FLOAT) //★★今回足した実装
return false;
return true;
}
else
return false;
...
修正後のコンパイラは浮動小数点数を60個使うコードを正常にコンパイルできます。たったこの3行を説明するだけで、えらい時間を費やしました。GCCは魔界ですね。
お気づきの方もいるかと思いますが、実は1つ上のelse if節にほぼ全く同じ判定文が既にあります。何も考えずに上からパクってMODEの名前を書き換えれば、今回の問題は直るんですけど、それだとどうして直るのか全くわからないんですよ……。
目次: GCC
前回(2020年7月6日の日記参照)はエラー出力のコードを追いましたが、エラーの原因とは無関係でした。
問題解決のヒントはRTLのダンプファイルにあります。エラーを起こす282r.reloadの1つ前のパス281r.iraのダンプ(オプション --dump-rtl-allで出力できます)を見ると下記のような記述が見つかります。
... Popping a57(r107,l0) -- assign reg 26 Popping a58(r106,l0) -- assign reg 27 Popping a59(r105,l0) -- assign reg 64 ★64はv0レジスタの番号 Popping a60(r104,l0) -- assign reg 65 ★65はv1レジスタの番号 Popping a61(r165,l0) -- assign reg 5 ...
レジスタ番号にベクトルレジスタが出てきます。とても怪しいですね。このメッセージを出しているコードを追います。
// gcc/ira-color.c
/* Pop the coloring stack and assign hard registers to the popped
allocnos. */
static void
pop_allocnos_from_stack (void)
{
ira_allocno_t allocno;
enum reg_class aclass;
for (;allocno_stack_vec.length () != 0;)
{
allocno = allocno_stack_vec.pop ();
aclass = ALLOCNO_CLASS (allocno);
if (internal_flag_ira_verbose > 3 && ira_dump_file != NULL)
{
fprintf (ira_dump_file, " Popping"); //★★Poppingはここで出力
ira_print_expanded_allocno (allocno);
fprintf (ira_dump_file, " -- ");
}
if (aclass == NO_REGS)
{
ALLOCNO_HARD_REGNO (allocno) = -1;
ALLOCNO_ASSIGNED_P (allocno) = true;
ira_assert (ALLOCNO_UPDATED_HARD_REG_COSTS (allocno) == NULL);
ira_assert
(ALLOCNO_UPDATED_CONFLICT_HARD_REG_COSTS (allocno) == NULL);
if (internal_flag_ira_verbose > 3 && ira_dump_file != NULL)
fprintf (ira_dump_file, "assign memory\n");
}
else if (assign_hard_reg (allocno, false)) //★★レジスタの割当をする
{
if (internal_flag_ira_verbose > 3 && ira_dump_file != NULL)
fprintf (ira_dump_file, "assign reg %d\n",
ALLOCNO_HARD_REGNO (allocno)); //★★assign reg 64はここで出力
}
// gcc/ira-build.c
/* This recursive function outputs allocno A and if it is a cap the
function outputs its members. */
void
ira_print_expanded_allocno (ira_allocno_t a)
{
basic_block bb;
//★★Poppingの後ろのaxx(rxx, lxx) を出力している
fprintf (ira_dump_file, " a%d(r%d", ALLOCNO_NUM (a), ALLOCNO_REGNO (a));
if ((bb = ALLOCNO_LOOP_TREE_NODE (a)->bb) != NULL)
fprintf (ira_dump_file, ",b%d", bb->index);
else
fprintf (ira_dump_file, ",l%d", ALLOCNO_LOOP_TREE_NODE (a)->loop_num);
if (ALLOCNO_CAP_MEMBER (a) != NULL)
{
fprintf (ira_dump_file, ":");
ira_print_expanded_allocno (ALLOCNO_CAP_MEMBER (a));
}
fprintf (ira_dump_file, ")");
}
// gcc/ira-int.h
#define ALLOCNO_NUM(A) ((A)->num)
#define ALLOCNO_REGNO(A) ((A)->regno)
...
#define ALLOCNO_HARD_REGNO(A) ((A)->hard_regno)
...
いずれもira_allocno_tのメンバを参照する。 (例) Popping a59(r105,l0) -- assign reg 64 - a59 : num - r105 : regno - l0 : loop_num - reg 64: hard_regno
割り当てたレジスタ番号はallocno->hard_regnoに格納されているようです。割り当てる関数はメッセージ出力のすぐ上にあるassign_hard_reg() 関数が行います。
// gcc/ira-color.c
static bool
assign_hard_reg (ira_allocno_t a, bool retry_p)
{
HARD_REG_SET conflicting_regs[2], profitable_hard_regs;
int i, j, hard_regno, best_hard_regno, class_size;
...
best_hard_regno = -1;
...
/* We don't care about giving callee saved registers to allocnos no
living through calls because call clobbered registers are
allocated first (it is usual practice to put them first in
REG_ALLOC_ORDER). */
mode = ALLOCNO_MODE (a);
for (i = 0; i < class_size; i++)
{
hard_regno = ira_class_hard_regs[aclass][i];
#ifdef STACK_REGS
if (no_stack_reg_p
&& FIRST_STACK_REG <= hard_regno && hard_regno <= LAST_STACK_REG)
continue;
#endif
if (! check_hard_reg_p (a, hard_regno,
conflicting_regs, profitable_hard_regs)) //★★このチェックを通過すると、割り当てる候補になる
continue;
cost = costs[i];
full_cost = full_costs[i];
if (!HONOR_REG_ALLOC_ORDER)
{
...
}
if (min_cost > cost)
min_cost = cost;
if (min_full_cost > full_cost
|| (!HONOR_REG_ALLOC_ORDER && min_full_cost == full_cost
&& best_hard_regno > hard_regno))
{
min_full_cost = full_cost;
best_hard_regno = hard_regno; //★★どのハードウェアレジスタに割り当てるか決まる
ira_assert (hard_regno >= 0);
}
if (internal_flag_ira_verbose > 5 && ira_dump_file != NULL)
fprintf (ira_dump_file, "(%d=%d,%d) ", hard_regno, cost, full_cost);
}
...
if (! retry_p)
restore_costs_from_copies (a);
ALLOCNO_HARD_REGNO (a) = best_hard_regno; //★★allocno->hard_regnoに格納
ALLOCNO_ASSIGNED_P (a) = true;
if (best_hard_regno >= 0)
update_costs_from_copies (a, true, ! retry_p);
ira_assert (ALLOCNO_CLASS (a) == aclass);
/* We don't need updated costs anymore. */
ira_free_allocno_updated_costs (a);
return best_hard_regno >= 0; //★★best_hard_regnoに何か値が入っていれば成功
}
関数assign_hard_reg() は色んな複雑なことをやっていますが、レジスタ割当の決め手は最後のループです。ループ内では基本的に先頭のレジスタから使います(※)。ループの先頭にcheck_hard_reg_p() というチェックがあって、このチェックをパスしたレジスタが割当の候補になります。
(※)厳密にはどのレジスタから割当を試みるかは、ira_class_hard_regsで決まります。この配列を初期化するのはsetup_class_hard_regs() で、コードを見た感じだと順序を変更することもできるようです。今回、詳細は調べていません。
// gcc/ira-color.c
/* Return true if HARD_REGNO is ok for assigning to allocno A with
PROFITABLE_REGS and whose objects have CONFLICT_REGS. */
static inline bool
check_hard_reg_p (ira_allocno_t a, int hard_regno,
HARD_REG_SET *conflict_regs, HARD_REG_SET profitable_regs)
{
int j, nwords, nregs;
enum reg_class aclass;
machine_mode mode;
aclass = ALLOCNO_CLASS (a);
mode = ALLOCNO_MODE (a);
if (TEST_HARD_REG_BIT (ira_prohibited_class_mode_regs[aclass][mode],
hard_regno)) //★★このチェックを通過したら割当候補
return false;
/* Checking only profitable hard regs. */
if (! TEST_HARD_REG_BIT (profitable_regs, hard_regno)) //★★このチェックは基本的に通過するはず
return false;
...
// gcc/ira-int.h
#define ALLOCNO_MODE(A) ((A)->mode)
...
#define ALLOCNO_CLASS(A) ((A)->aclass)
...
ベクトルレジスタ(番号64以降)がcheck_hard_reg_p() に渡されるときのaclass, modeの値を見ておきます。今はどうでも良いんですが、次の章で使います。
ブレークに設定する条件はif hard_regno == 64 (gdb) p aclass $4 = ALL_REGS (gdb) p mode $5 = E_SFmode
チェックのキモはira_prohibited_class_mode_regsで、レジスタ番号が示す位置のビットがセットされていると、チェックに引っかかって、割り当て候補から外れます。この配列を初期化している箇所はsetup_prohibited_class_mode_regs() です。
// gcc/ira.c
static void
setup_prohibited_class_mode_regs (void)
ira_prohibited_class_mode_regs
{
int j, k, hard_regno, cl, last_hard_regno, count;
for (cl = (int) N_REG_CLASSES - 1; cl >= 0; cl--)
{
temp_hard_regset = reg_class_contents[cl] & ~no_unit_alloc_regs;
for (j = 0; j < NUM_MACHINE_MODES; j++)
{
count = 0;
last_hard_regno = -1;
CLEAR_HARD_REG_SET (ira_prohibited_class_mode_regs[cl][j]);
for (k = ira_class_hard_regs_num[cl] - 1; k >= 0; k--)
{
hard_regno = ira_class_hard_regs[cl][k];
if (!targetm.hard_regno_mode_ok (hard_regno, (machine_mode) j)) //★★hard_regno_mode_ok() がtrueであれば割当候補になる
SET_HARD_REG_BIT (ira_prohibited_class_mode_regs[cl][j],
hard_regno);
else if (in_hard_reg_set_p (temp_hard_regset,
(machine_mode) j, hard_regno))
{
last_hard_regno = hard_regno;
count++;
}
}
ira_class_singleton[cl][j] = (count == 1 ? last_hard_regno : -1);
}
}
}
// gcc/config/riscv/riscv.h
enum reg_class
{
NO_REGS, /* no registers in set */
SIBCALL_REGS, /* registers used by indirect sibcalls */
JALR_REGS, /* registers used by indirect calls */
GR_REGS, /* integer registers */
FP_REGS, /* floating-point registers */
VP_REGS, /* vector registers */
FRAME_REGS, /* arg pointer and frame pointer */
ALL_REGS, /* all registers */
LIM_REG_CLASSES /* max value + 1 */
};
#define N_REG_CLASSES (int) LIM_REG_CLASSES
配列の名前prohibitedの通り、この配列のビットがセットされているレジスタは割り当て「禁止」です。最初に配列のビットをクリアし(許可状態)、条件targetm.hard_regno_mode_ok() がtrueだったら変更せず(許可状態のまま)、falseだったらビットをセット(禁止状態)します。
次回は原因を修正します。
目次: GCC
以前(2020年3月27日の日記参照)ベクトルレジスタの定義を追加したとき、riscv_hard_regno_mode_ok() を変更しました。machine modeは無視して、ベクトルレジスタを指すレジスタ番号だったらとにかくtrueを返すようにしましたが、実はこの実装は正しくありません。
例えば以下のようなプログラムを書くと、おかしなことが起こります。
void _start(void)
{
static volatile float gf[60];
float f00, f01, f02, f03, f04, f05, f06, f07, f08, f09;
float f10, f11, f12, f13, f14, f15, f16, f17, f18, f19;
float f20, f21, f22, f23, f24, f25, f26, f27, f28, f29;
float f30, f31, f32, f33, f34, f35, f36, f37, f38, f39;
float f40, f41, f42, f43, f44, f45, f46, f47, f48, f49;
float f50, f51, f52, f53, f54, f55, f56, f57, f58, f59;
__asm__ volatile("nop;\n");
f00 = gf[ 0]; f01 = gf[ 1]; f02 = gf[ 2]; f03 = gf[ 3]; f04 = gf[ 4];
f05 = gf[ 5]; f06 = gf[ 6]; f07 = gf[ 7]; f08 = gf[ 8]; f09 = gf[ 9];
f10 = gf[10]; f11 = gf[11]; f12 = gf[12]; f13 = gf[13]; f14 = gf[14];
f15 = gf[15]; f16 = gf[16]; f17 = gf[17]; f18 = gf[18]; f19 = gf[19];
f20 = gf[20]; f21 = gf[21]; f22 = gf[22]; f23 = gf[23]; f24 = gf[24];
f25 = gf[25]; f26 = gf[26]; f27 = gf[27]; f28 = gf[28]; f29 = gf[29];
f30 = gf[30]; f31 = gf[31]; f32 = gf[32]; f33 = gf[33]; f34 = gf[34];
f35 = gf[35]; f36 = gf[36]; f37 = gf[37]; f38 = gf[38]; f39 = gf[39];
f40 = gf[40]; f41 = gf[41]; f42 = gf[42]; f43 = gf[43]; f44 = gf[44];
f45 = gf[45]; f46 = gf[46]; f47 = gf[47]; f48 = gf[48]; f49 = gf[49];
f50 = gf[59]; f51 = gf[51]; f52 = gf[52]; f53 = gf[53]; f54 = gf[54];
f55 = gf[55]; f56 = gf[56]; f57 = gf[57]; f58 = gf[58]; f59 = gf[59];
__asm__ volatile("nop;\n");
gf[ 0] = f00; gf[ 1] = f01; gf[ 2] = f02; gf[ 3] = f03; gf[ 4] = f04;
gf[ 5] = f05; gf[ 6] = f06; gf[ 7] = f07; gf[ 8] = f08; gf[ 9] = f09;
gf[10] = f10; gf[11] = f11; gf[12] = f12; gf[13] = f13; gf[14] = f14;
gf[15] = f15; gf[16] = f16; gf[17] = f17; gf[18] = f18; gf[19] = f19;
gf[20] = f20; gf[21] = f21; gf[22] = f22; gf[23] = f23; gf[24] = f24;
gf[25] = f25; gf[26] = f26; gf[27] = f27; gf[28] = f28; gf[29] = f29;
gf[30] = f30; gf[31] = f31; gf[32] = f32; gf[33] = f33; gf[34] = f34;
gf[35] = f35; gf[36] = f36; gf[37] = f37; gf[38] = f38; gf[39] = f39;
gf[40] = f40; gf[41] = f41; gf[42] = f42; gf[43] = f43; gf[44] = f44;
gf[45] = f45; gf[46] = f46; gf[47] = f47; gf[48] = f48; gf[49] = f49;
gf[50] = f50; gf[51] = f51; gf[52] = f52; gf[53] = f53; gf[54] = f54;
gf[55] = f55; gf[56] = f56; gf[57] = f57; gf[58] = f58; gf[59] = f59;
__asm__ volatile("nop;\n");
}
最適化O1以上でビルドするとinternal compile errorが発生します。
$ riscv32-unknown-elf-gcc b.c -march=rv32imafcv -mabi=ilp32f -O3 -Wall -fno-builtin -fno-inline -nostdlib -g b.c: In function '_start': b.c:52:1: error: insn does not satisfy its constraints: 52 | } | ^ (insn 562 17 18 2 (set (reg/v:SF 65 v1 [orig:104 f00 ] [104]) (reg/v:SF 47 fa5 [orig:104 f00 ] [104])) "b.c":23:6 153 {*movsf_hardfloat} (nil)) during RTL pass: reload dump file: b.c.282r.reload b.c:52:1: internal compiler error: in extract_constrain_insn, at recog.c:2195 0x11b9e4d _fatal_insn(char const*, rtx_def const*, char const*, int, char const*) gcc/gcc/rtl-error.c:108 0x11b9ed1 _fatal_insn_not_found(rtx_def const*, char const*, int, char const*) gcc/gcc/rtl-error.c:118 0x1130298 extract_constrain_insn(rtx_insn*) gcc/gcc/recog.c:2195 0xeabdd8 check_rtl gcc/gcc/lra.c:2167 0xead5c0 lra(_IO_FILE*) gcc/gcc/lra.c:2604 0xe1a63b do_reload gcc/gcc/ira.c:5523 0xe1b078 execute gcc/gcc/ira.c:5709
エラーが発生している箇所はreloadで、エラーの原因となったRTLも出力されています。RTLのsetを見ると、宛先がreg/v:SF 65 v1になっています。浮動小数点の処理のはずなのに、どうしてベクトルレジスタが選択されているのでしょうか?
エラーを出力するのは関数check_rtl() です。lra() の最初あたりcheck_rtl(false) と、最後あたりcheck_rtl(true) の2回呼ばれます。エラーが発生しているのは、2回目の呼び出しです。
// gcc/lra.c
/* Function checks RTL for correctness. If FINAL_P is true, it is
done at the end of LRA and the check is more rigorous. */
static void
check_rtl (bool final_p)
{
basic_block bb;
rtx_insn *insn;
lra_assert (! final_p || reload_completed);
FOR_EACH_BB_FN (bb, cfun)
FOR_BB_INSNS (bb, insn)
if (NONDEBUG_INSN_P (insn)
&& GET_CODE (PATTERN (insn)) != USE
&& GET_CODE (PATTERN (insn)) != CLOBBER
&& GET_CODE (PATTERN (insn)) != ASM_INPUT)
{
if (final_p)
{
extract_constrain_insn (insn); //★★これ
continue;
}
// gcc/recog.c
/* Do uncached extract_insn, constrain_operands and complain about failures.
This should be used when extracting a pre-existing constrained instruction
if the caller wants to know which alternative was chosen. */
void
extract_constrain_insn (rtx_insn *insn)
{
extract_insn (insn); //★★INSN RTLからrecog_dataを作成する
if (!constrain_operands (reload_completed, get_enabled_alternatives (insn))) //★★ここでfalseが返る
fatal_insn_not_found (insn); //★★internal compile errorが出力される
}
おさらいですが、エラーが発生するときのinsnの先頭は下記のようなRTLでした。
(insn 562 17 18 2 (set (reg/v:SF 65 v1 [orig:104 f00 ] [104])
(reg/v:SF 47 fa5 [orig:104 f00 ] [104])) "b.c":23:6 145 {*movsf_hardfloat}
(nil))
関数constrain_operands() は何度も呼ばれるので、エラーが起きるRTLが来たときUID 562で止めます。INSN_UID(insn) == 562が来たときに止められるようにコードを少し改変し、ブレークポイントを仕掛けます。
もしくはINSN_UID(insn) はデバッガから参照するときはinsn->u2.insn_uidで見えますから、条件ブレークでも止められると思います。
// gcc/lra.c
/* Function checks RTL for correctness. If FINAL_P is true, it is
done at the end of LRA and the check is more rigorous. */
static void
check_rtl (bool final_p)
{
basic_block bb;
rtx_insn *insn;
lra_assert (! final_p || reload_completed);
FOR_EACH_BB_FN (bb, cfun)
FOR_BB_INSNS (bb, insn)
if (NONDEBUG_INSN_P (insn)
&& GET_CODE (PATTERN (insn)) != USE
&& GET_CODE (PATTERN (insn)) != CLOBBER
&& GET_CODE (PATTERN (insn)) != ASM_INPUT)
{
if (INSN_UID(insn) == 562) //★★ブレークポイントを仕掛けるために追加した部分
printf("check point!!\n"); //★★ブレークポイントを仕掛けるために追加した部分
if (final_p)
{
extract_constrain_insn (insn);
continue;
}
関数constrain_operands() を追ってみると、RTLが取るオペランド(今回は2つ取っている)に対応するconstraintsをチェックしています。recog_data.constraintsを見るとconstraintsがわかります。
コードは以前(2020年3月29日の日記参照)見たprocess_alt_operands() の前半部分に似た作りとなっています。同じようなコードを色々なところに書くのはやめてほしいですね……。
(gdb) p recog_data.constraints $6 = { 0x2d717f9 "=f,f,f,m,m,*f,*r,*r,*r,*m", 0x2d71813 "f,G,m,f,G,*r,*f,*G*r,*m,*r", ...
このconstraintsは降って湧いたものではなくて、下記の *movsf_hardfloatの定義が由来です。
// gcc/config/riscv/riscv.md
(define_insn "*movsf_hardfloat"
[(set (match_operand:SF 0 "nonimmediate_operand" "=f,f,f,m,m,*f,*r, *r,*r,*m")
(match_operand:SF 1 "move_operand" " f,G,m,f,G,*r,*f,*G*r,*m,*r"))]
...
浮動小数点の移動の定義ですから当たり前なんですけど、ベクトルレジスタを受け付けるconstraints 'v' は含まれません。なのにベクトルレジスタを渡そうとしているのでconstrain_operands() が怒っているわけです。
と、ここまでエラー出力に至るまでを追いましたが、残念なことにエラーの原因はreloadパスにはありません。エラーが出ている箇所と、エラーの原因が全く関係ない現象はGCCでは珍しくなくて、いつも解析が難しくて困っています。
振り出しに戻ってしまいました。続きは次回。
目次: ベンチマーク
昨日(2020年7月4日の日記参照)はSEGVするELFバイナリサイズを92バイトまで削ることができました。
その後、色々弄っていて見つけたのですが、プログラムヘッダのtypeをNULLにしておけば、ファイルサイズやオフセットがめちゃくちゃでもexecveは文句を言わないっぽいみたいです。
ELFヘッダのe_identの後半8バイトは0(前半を書き換えるとELFと認識されなくなります)なので、この部分をプログラムヘッダだよ、と指定すればtype NULLに解釈されます。
これで64バイト、つまりELFヘッダしかない実行ファイルができました。何の役にも立たないですけどね……。
$ ls -la a.out -rwxrwxr-x+ 1 katsuhiro katsuhiro 64 Jul 3 06:39 a.out
これ以上1バイトでも削るとELFヘッダの長さを下回るため、ELFバイナリとして成立しません。よって64バイトが最短だと思いますが、私が気づいていない裏技があるかもしれません。
このファイルを実行してみると、システムコールexecveがエラーを返さないで進むので、ELFファイルとして認識してもらえているようです。
$ strace ./a.out execve("./a.out", ["./a.out"], 0x7ffedf7dfa90 /* 47 vars */) = 0 --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x400000001} --- +++ killed by SIGSEGV +++ Segmentation fault
$ readelf -a a.out ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400000001 Start of program headers: 8 (bytes into file) Start of section headers: 0 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 1 Size of section headers: 0 (bytes) Number of section headers: 0 Section header string table index: 0 There are no sections in this file. There are no sections to group in this file. Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align NULL 0x00000001003e0002 0x0000000400000001 0x0000000000000008 0x0000000000000000 0x0038004000000000 0x1 There is no dynamic section in this file. There are no relocations in this file. The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not curr ently supported. Dynamic symbol information is not available for displaying symbols. No version information found in this file.
一応readelfでも読めますが、プログラムヘッダのオフセットやアドレスはめちゃくちゃです。
目次: ベンチマーク
昨日(2020年7月3日の日記参照)試したところによると、C言語は0文字でSEGVするプログラムを書けました。
では、出力されたバイナリだと最短はいくつでしょうか?とりあえずベースとしてC言語の0文字SEGVプログラムの出力を見ます。
$ gcc -nostdlib a.c /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000 $ ls -la a.out -rwxr-xr-x 1 katsuhiro katsuhiro 9528 Jul 3 04:11 a.out
なんと9KBもあります。readelfで見るとダイナミックリンカー関連のシンボルが含まれているようなので、staticオプションを付けてダイナミックリンカー関連のシンボルを消します。
$ echo -n > a.c && gcc -nostdlib -static a.c /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000 $ ls -la a.out -rwxr-xr-x 1 katsuhiro katsuhiro 968 Jul 3 04:12 a.out $ strace ./a.out execve("./a.out", ["./a.out"], 0x7ffc92c2b390 /* 46 vars */) = 0 --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x401000} --- +++ killed by SIGSEGV +++ Segmentation fault
ログが大量に出るはずのstraceも、わずか1行しかログが出ません。何もしないバイナリにも関わらずa.outのサイズは1KB近くあります。
お手軽なバイナリのダイエットとして、実行に不要なセクションをstripします。
$ strip -R ".note.gnu.build-id" -R ".comment" a.out $ ls -la a.out -rwxr-xr-x 1 katsuhiro katsuhiro 376 Jul 3 04:18 a.out
それでも376バイト。まだでかいですね。
この376バイトのファイルを元にして、バイナリを手で削ります。加工の方針としては、
結果92バイトになりました。
$ ls -la a.out -rwxrwxr-x+ 1 katsuhiro katsuhiro 92 Jul 3 05:43 a.out
もっとアグレッシブにELFヘッダとプログラムヘッダを重ねれば、64バイトにできるかもしれません。
このファイルを実行してみると、システムコールexecveがエラーを返さないで進むので、ELFファイルとして認識してもらえているようです。
$ strace ./a.out execve("./a.out", ["./a.out"], 0x7fff2b4f74a0 /* 47 vars */) = 0 --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x400000001} --- +++ killed by SIGSEGV +++ Segmentation fault
$ readelf -a a.out ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400000001 Start of program headers: 36 (bytes into file) Start of section headers: 0 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 1 Size of section headers: 0 (bytes) Number of section headers: 0 Section header string table index: 0 There are no sections in this file. There are no sections to group in this file. Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align NULL 0x0000000000000000 0x0000000100380040 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x1000 There is no dynamic section in this file. There are no relocations in this file. The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported. Dynamic symbol information is not available for displaying symbols. No version information found in this file.
一応readelfでも読めますし、そこそこ真っ当なファイルをキープできていると思います。
SEGVを出すのがTwitterで流行しているみたいなので、少し考えてみました。Twitterで見かけたのは、*a;main(){*a=0;}(16文字)です。最適化オプションにもよりますが、異常なアドレスへのmovか、未定義命令ud2が出力されてSEGVします。
$ echo -n '*a;main(){*a=0;}' > a.c && gcc a.c a.c:1:1: warning: data definition has no type or storage class 1 | *a;main(){*a=0;} | ^ a.c:1:2: warning: type defaults to 'int' in declaration of 'a' [-Wimplicit-int] 1 | *a;main(){*a=0;} | ^ a.c:1:4: warning: return type defaults to 'int' [-Wimplicit-int] 1 | *a;main(){*a=0;} | ^~~~ $ ./a.out Segmentation fault
ただSEGVするだけで良ければ、mainのアドレスを .textではないアドレスにすれば良いので、main;(5文字)でも、達成できます。
$ echo -n 'main;' > a.c && gcc a.c a.c:1:1: warning: data definition has no type or storage class 1 | main; | ^~~~ a.c:1:1: warning: type defaults to 'int' in declaration of 'main' [-Wimplicit-int] $ ./a.out Segmentation fault
この例はmainを関数ではなく変数として定義し、mainをbssセクションに配置します。最近のLinuxならばデータが置かれているセグメントは実行禁止にするはずなので、mainが指すデータが何であろうと、ジャンプした瞬間にSEGV します。
趣旨とは外れますがSEGVを避けて正常終了させたければ、
$ echo -n 'main=0xc3;' > a.c && gcc -z execstack a.c a.c:1:1: warning: data definition has no type or storage class 1 | main=0xc3; | ^~~~ a.c:1:1: warning: type defaults to 'int' in declaration of 'main' [-Wimplicit-int] $ ./a.out (SEGVしない)
変数mainが指す位置にretq命令(0xc3)を置いて、-z execstackオプションでデータ領域を実行可能にしています。attributeで .text領域に置いても実行できますが、そんなことするくらいならmain() 関数を書いた方が短いです。
変化球を使ってよければ0文字でSEGVできます。nostdlibオプションを使います。
$ echo -n > a.c && gcc -nostdlib a.c /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000 $ ./a.out Segmentation fault
GNU ld(他のリンカーでも同じだと思いますけど)はELFのエントリアドレスに _startというシンボルのアドレスを使います。通常はcrt.oなど、リンク時に自動的に追加されるオブジェクトが _startを定義しますが、nostdlibオプションによりcrt.oがリンクされなくなって、_startは未定義になります。
するとldは _startを適当なアドレスに設定(上記の例では0x1000に)します。当然 _startが指すアドレスにコードはありませんから、実行するとクラッシュします。
目次: ゲーム
Steamの買い物カートの中身一覧を見る方法を探していたんですが、全くわかりませんでした。だいぶ彷徨いましたが、未だトップ画面からカートを見る方法はわからないままです。Steamは本当にUIがひどい。Amazonを見習ってくれ……。
その際に副産物として、秘密の実績を確認する方法を見つけたのでメモしておきます。
と辿ると自分が所持しているゲームの一覧が出ますから、実績を見たいゲームの
と辿ると全ての実績が表示されます。
秘密の実績の意味とは一体……??
< | 2020 | > | ||||
<< | < | 07 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | - | 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | - |
合計:
本日: