PalmOS 5.x Hack開発講座 第7回 「メニューの文字化けを直す」その3
前回からの続きです 肝心要のARMに関係のあるところを見てゆきましょう
3.3.ARMに関係のあるところを... [MenuAddItem-03e9]、[ResLoadMenu-03e8]ターゲット
3.3.3.OS5における関数呼び出しの方法 レジスタ、スタックの取り扱い
またちょっと横道にそれて、OS5における関数呼び出しの方法について、引数の渡し方、戻り値の戻し方をお話しましょう スタックを自前で確保する場合には、これらの引数、戻り値を一切壊してはいけませんので、この関数呼び出しの方法をきちんと把握しておかなければいけません
さて、最近のプロセッサには、プロセッサに接続されている外部メモリ以外に、高速にアクセスできる内部メモリ(レジスタ)を持っているのが一般的です
僕たちが対象としているARMプロセッサについても同様で、ユーザが自由に利用できる内部メモリは全部で64バイトあります
具体的には、1ワードx16個で、それぞれr0、r1、...、r15という識別子がつけられています(ARMアーキテクチャの1ワードは4バイト、見慣れた表現をするとUInt32ですね) ただし、全部で16個あるといっても丸々使えるわけではなく、ある決まりごとに従って使われていますので多少の制限が存在します
[r0]、[r1]、[r2]、[r3]
関数呼び出しの引数に利用されるスクラッチレジスタ
[r4]、[r5]、[r6]、[r7]、[r8]
関数内で値の一時記憶に利用される汎用レジスタ
[r9]
PalmOSではシステムグローバル領域の末尾アドレスを格納するために使われていますので、これは利用不可 前回出てきましたね[r10]
これもPalmOSに限った話ですが、実行しているアプリケーションの先頭アドレスが格納されます
各アプリケーションの中で、内部関数のアドレス計算に使用されたり、大域変数のアドレス計算に利用されたりしますので、ARMletアプリケーション中のARMletコードの中では重要な役割を担っています
この[r10]レジスタをいじってあげるとARMletコード中でグローバル変数の取り扱いができるようになりますので、興味のある方は[ARMlet_Startup.c]、[PNOLoader.c]あたりを覗いて見られるとよいかと思います この[r10]レジスタのお話はまた別の機会に[r11]、[r12]
汎用レジスタですが、[r11]はあまり使われていないみたいですね [r12]はAPIコールの際に利用されるレジスタでAPIコールに伴って破壊されるということを承知の上であればスクラッチレジスタとして利用できます これも前回出てきましたね[r13] 別名[sp]
別名スタックポインタとして[sp]と表現されます スタックにアクセスするために利用されるレジスタです
今回はこの[SP]をいじります[r14] 別名[lr]
別名リンクレジスタとして[lr]、リターンアドレスを格納しておくレジスタとして使用されますが、気をつけて使えば汎用レジスタとして使うこともできます[r15] 別名[pc]
別名プログラムカウンタとして[pc]で表現されます 名前のとおり、プログラムカウンタがセットされていますので、現在の自分の位置がメモリ上のどこにあるのかを知ることができますし、この[pc]に値をセットすることで目的のアドレスにプログラムカウンタを移動する、つまりジャンプすることができます
まあ、いろいろありますが、ユーザが自由に使えるのは[r0]から[r8]の合計9個のレジスタと、スタックだけであるととらえておけばよいでしょう
そうそう、スタックとはなんぞやということをお話しておかなければいけませんね
スタックというのは、限られた数のレジスタを利用しているだけではどうにもならないときに利用されるメモリーのことです
レジスタに収まらない数の値を扱うときは、一時的に使わない値をレジスタからスタックに積んでそのレジスタを開放してあげて、スタックに積んでしまった値を利用するときにはスタックからレジスタに呼び戻してくるという使われ方をします 例えるならば、Windowsのスワップファイルのような存在と思っていただければよいでしょう
さて、通常の関数では、関数の入り口と出口でお約束事の手続きを踏んでいます ちょっとアセンブリコードで書いてみますね
asm void myFnc(UInt32 arg1) { stmfd sp!, {r4-r7,lr} ... いろいろな処理 ... ldmfd sp!, {r4-r7,lr} bx lr }
入り口で、関数の中で破壊してしまうレジスタをスタックに積んで、出口ではスタックに積んである値をレジスタに書き戻してリターン
重要なポイントは、関数が呼ばれるたびにスタックを消費してゆくということであります 関数がネストしている場合、再起呼び出しが行われている場合はどうなるでしょうか...
そうです、スタックにどんどん値が積まれていって、しまいにはスタックのために確保されている領域の範囲を超えてしまいます
通常のアプリケーションでは、アプリケーション内部で消費されるスタックを概算して、十分な領域を確保していますが、Hackの場合はどうでしょうか
アプリケーション側はHackによって消費されるスタックは考慮に入れていないので、あまり複雑なことをHackの中で行うとスタック領域が足りなくなってしまう場合が出てくるのです
そこで、今回お話しする「スタック領域を自前で確保」という処理が必要になってきます
なにやら難しそうですが、簡単といえば簡単 [MemPtrNew]でメモリーを確保して[sp]を新しく確保した領域にすげ替えてあげればよいだけなのです
といっても、[MemPtrNew]を呼ぶだけでもレジスタを破壊してしまいますので、破壊するレジスタをどこかに退避しておかなければいけません が、退避するにはスタックを使用しますし...
とりあえず、関数呼び出しについてお話を戻しましょう
例えば、次のような関数があるとします
UInt32 myFnc(UInt32 arg1, UInt32 arg2, UInt32 arg3, UInt32 arg4, UInt32 arg5, UInt32 arg6);
この関数を呼び出すときには、レジスタとスタックが利用され、次のような状態になります
r0 = arg1 r1 = arg2 r2 = arg3 r3 = arg4 [sp] = arg5 //スタックポインタに格納されているアドレスにarg5をセット [sp+4] = arg6 //スタックポインタに格納されているアドレス+4にarg6をセット lr = pc
関数から戻ってきたときには
r0 = returnvalue //関数の戻り値がセットされている r1 = ... r2 = ... r3 = ... [sp] = arg5 [sp+4] = arg6 lr = pc
他のレジスタには呼び出しもとの関数内部で何らかの値が格納されています
さて、スタックを自前で確保する場合に重要なポイントは、
1.トラップしている関数が呼ばれたときのレジスタ、スタックの状態をそっくりそのまま維持しなければいけない
2.トラップしている関数から戻るときにはレジスタ、スタックの状態をきちんと元通りに戻さなければいけない
という二点になります
簡単そうですが、ことは結構ややこしいのです
・トラップしている関数が呼ばれたときには、関数の引数がスタックに積まれていますので、例えばリターンアドレスをスタックに積むだけでもおかしなことになってしまう
・レジスタの状態をそっくりそのまま維持しなくてはいけないといっても、何かしようとすれば必ずいくつかのレジスタを破壊してしまう
・もともとのスタックポインタの値と、新しく確保するスタックのベースアドレスはどこかに記録しておかないと元に戻すことができない
・といっても、スタックは使えない...
うむむむむ... 興味があれば、僕の用意したコードを見ないで考えてみてください かなりややこしいですよ
このお話はこの辺にして、自前でスタックを確保する部分に進みましょう
今回用意したプロジェクトから[NewStack.h]と[NewStack.c]をひっぱってきましょう
まず、[NewStack.h] これはプロトタイプ宣言だけですね
// NewStack.h #ifndef NEWSTACK_H #define NEWSTACK_H #include "ArmApiCall.h" #ifdef __cplusplus extern "C" { #endif //__cplusplus void __NewStack__(void); void __RestoreStack__(void); #ifdef __cplusplus } #endif //__cplusplus #endif //NEWSTACK_H
もう一個、[NewStack.c]
// NewStack.c #include "NewStack.h" #ifndef NewStackSize #define NewStackSize #1024*4 // default new-stack size = 4KByte #endif #ifndef CopySize #define CopySize #14*4 // default copy arg5 to arg9 #endif #pragma thumb off asm void __NewStack__(void) { stmfd sp!,{r0-r6,lr} // store r0 to r6 and lr mov r0, NewStackSize bl MemPtrNew // allocate new-stack mov r4, r0 // store newStackBase to r4 add r5, r0, NewStackSize // now r5 point to new-sp stmfd r5!,{r4, sp} // store newStackBase, org-sp to newStack sub r5, r5, CopySize // shift new-sp mov r0, r5 // copy to new-sp mov r1, sp // copy from org-sp mov r2, CopySize // copy-size bl MemMove // copy data to new-sp from org-sp mov sp, r5 // replace sp to new-sp ldmfd sp!,{r0-r6,lr} // restore bloken register r0 to r6 and lr add sp, sp, #4 // no-need lr. it's stored in org-sp bx lr } asm void __RestoreStack__(void) { stmfd sp!,{r0,lr} // store r0 and lr ldr r0, [sp,CopySize - 28] // pop new-stack base address bl MemPtrFree // free up ldmfd sp!,{r0,lr} // restore r0 and lr ldr sp, [sp,CopySize - 32] // pop org-sp and restore add sp, sp, #32 // prepair sp must point to bx lr } #pragma thumb reset
なにやら難しそうなことをしているのはお分かりいただけるでしょうか お話しますといっておいてなんですが、解説はやめておきましょうね パズルと思って解読してみてください
[NewStackSize]と[CopySize]の値はケースバイケースで変更する必要があるでしょう デフォルトの値は、それぞれ4Kバイト、56バイト(トラップしている関数の引数が9個まで対応できます)となっています
変更する場合のお勧めの方法は、任意の値で[NewStackSize]と[CopySize]を定義したヘッダファイルを一つ用意して、ターゲットの設定で[Prefix File:]に設定しておく方法です 今回のプロジェクトでは[ResLoadMenuSpSize.h]というヘッダファイルを用意しています
// ResLoadMenuSpSize.h #define NewStackSize #1024*12 // new-stack size = 12KByte #define CopySize #9*4 // Num arg. = 1
さてさて、なんとなくおわかりでしょうか? まあ、よくわからなくても大丈夫 肝心なのは、使い方ですから
[ResLoadMenu.cpp]からエントリポイントを抜粋してきました
extern "C" void __ARMlet_Startup__(void); #include "NewStack.h" #pragma thumb off asm void __ARMlet_Startup__(void) { stmfd sp!,{lr} bl __NewStack__ // allocate new-stack bl __myApiEntry__ // call myApi with new-stack bl __RestoreStack__ // restore stack ldmfd sp!,{lr} bx lr } #pragma thumb reset
[MenuAddItem]では[b __myApiEntry__]で置き換え関数を呼んでいたのに対して、こちらでは[bl]というブランチアンドリターンというサブルーチンコールを用いています
あとは、[bl __NewStack__]と[bl __RestoreStack__]で[bl __myApiEntry__]をはさんであげるだけ
アプリケーションで[MenuAddItem]がコールされると、この[__ARMlet_Startup__]に突入し、新しいスタックを確保するコード[__NewStack__]に飛んで、次に[myApiResLoadMenu]の処理をして、新しく確保したスタックの開放と元のスタックに戻す処理[__RestoreStack__]に飛んで、戻ってきたら呼び出しもとのアプリケーションに戻る という流れになります
もう、この辺はブラックボックスとして扱ってもかまわないとは思いますが、一度アセンブラを勉強していろいろと考えてみるのは面白いとおもいます
さて、最後のHackコードのお話に移りましょう
やっと最後にたどりつきました
まず、コードを見てみましょう
// ResLoadMenu.cpp #define ALLOW_ACCESS_TO_INTERNALS_OF_MENUS_OS5 #include <PalmOS.h> #include "MenuOS5.h" #include "ArmApiCall.h" #include "ARMRes.h" #define RESID 0x03e8 #define CRID 'To-f' #define myApiResLoadMenu __myApiEntry__ void* myApiResLoadMenu(UInt16 rscID); typedef void* (*osApi)(UInt16 rscID); UInt32 prepTof(UInt8* from, UInt8* to); void* myApiResLoadMenu(UInt16 rscID) { osApi osApiResLoadMenu; FtrGet(CRID, RESID, (UInt32*)&osApiResLoadMenu); MenuBarPtrOS5 menuP = (MenuBarPtrOS5)osApiResLoadMenu(rscID); UInt8* from = (UInt8*)menuP->menus[0].title; UInt8* to = (UInt8*)((UInt32)menuP + MemPtrSize(menuP)); UInt32 count = prepTof(from, to); if(count) { DmOpenRef dbRef = DmOpenDatabaseByTypeCreator('HACK', CRID, dmModeReadOnly); if(dbRef) { Char t[10]; StrIToA(t, count); FrmCustomAlert(FoundMessage, t, "", ""); DmCloseDatabase(dbRef); } } return menuP; } #pragma thumb on UInt32 prepTof(UInt8* from, UInt8* to) { UInt32 which = 0; FtrGet(CRID, 0, &which); UInt8 replaceTo; switch(which) { case 0: replaceTo = chrEllipsis; break; case 1: replaceTo = '_'; break; case 2: replaceTo = '-'; } UInt32 count = 0; #include <CharLatin.h> for(UInt8* i = from; i < to; i++) { if(i[0] == chrHorizontalEllipsis) { if(i[1] == chrNull) { i[0] = replaceTo; count++; } } else if(i[0] == chrFullStop) { if((i[1] == chrFullStop) && (i[2] == chrFullStop) && (i[3] == chrNull)) { i[0] = replaceTo; i[1] = chrNull; i += 3; count++; } } } return count; } #pragma thumb reset // Don't care below ----------------------- // Simple Startp code --------------------- // Allocate new-stack and call myApi ------ // Always compile as ARM instruction set -- extern "C" void __ARMlet_Startup__(void); #include "NewStack.h" #pragma thumb off asm void __ARMlet_Startup__(void) { stmfd sp!,{lr} bl __NewStack__ // allocate new-stack bl __myApiEntry__ // call myApi with new-stack bl __RestoreStack__ // restore stack ldmfd sp!,{lr} bx lr } #pragma thumb reset
まあ、だいたい眺めれば何をやっているかはわかるでしょう 文字化けを検索する対象であるテキストは、メニューリソースの後ろのほうにまとめられていますので、テキストの先頭からリソースの末尾までを検索対象にして一気に文字化け検索と修正を行っているだけです
そうそう、Hackの設定パネルで行った設定に応じて、化け文字を置き換える文字を変更する処理も加わっています 設定値の受け取りは、Hackでおなじみのフィーチャ経由ですね
はあ、めでたしめでたし 一通りのお話が終わりました
...と終わらせるわけにはいきませんね [MenuBarPtrOS5]の存在に気が付きましたか?
第3回でも書きましたが、幾つかの構造体には変更が加わっています 今回ターゲットとしている[MenuBarType]構造体もその一つでした 今回は、新しくなった[MenuBarType]とそれに関連する構造体を新しく定義して使用しています コードの先頭でインクルードしている"MenuOS5.h"に記述してありますので目を通してください
// MenuOS5.h #ifndef __MENUOS5_H__ #define __MENUOS5_H__ #include <PalmTypes.h> #include <Window.h> typedef struct MenuItemTagOS5 #ifdef ALLOW_ACCESS_TO_INTERNALS_OF_MENUS_OS5 { UInt16 id; Char command; UInt8 hidden; // Char *itemStr; } #endif MenuItemTypeOS5; typedef struct MenuPullDownTagOS5 #ifdef ALLOW_ACCESS_TO_INTERNALS_OF_MENUS_OS5 { WinHandle menuWin; RectangleType bounds; WinHandle bitsBehind; RectangleType titleBounds; Char *title; UInt16 hidden; // UInt16 numItems; // MenuItemTypeOS5 *items; } #endif MenuPullDownTypeOS5; typedef MenuPullDownTypeOS5 *MenuPullDownPtrOS5; typedef struct MenuBarAttrTagOS5 #ifdef ALLOW_ACCESS_TO_INTERNALS_OF_MENUS_OS5 { UInt16 visible :1; UInt16 commandPending :1; UInt16 insPtEnabled :1; UInt16 needsRecalc :1; UInt16 attnIndicatorIsAllowed :1; UInt16 reserved :11; } #endif MenuBarAttrTypeOS5; typedef struct MenuBarOS5Tag #ifdef ALLOW_ACCESS_TO_INTERNALS_OF_MENUS_OS5 { WinHandle barWin; WinHandle bitsBehind; WinHandle savedActiveWin; WinHandle bitsBehindStatus; MenuBarAttrTypeOS5 attr; Int16 curMenu; Int16 curItem; Int16 numMenus; // Int32 commandTick; // MenuPullDownPtrOS5 menus; } #endif MenuBarTypeOS5; typedef MenuBarTypeOS5 *MenuBarPtrOS5; #endif
新しくなった構造体を調べるにはいろいろな方法がありますが、UIリソースにからむ構造体についてはPilRCを使って調べるとよいでしょう
具体的には、全く同じリソースを[-LE32]オプションをつけてコンパイルしたものと、つけないでコンパイルしたものを見比べて、経験と勘で組み立てなおすという作業になります
ただ、変更されたといっても、あまり大きな変更は行われていません ARMアーキテクチャのメモリアクセスの決まりごとの元では無駄なパディングが入ってしまう場合にはメンバの順番を入れ替えて、また将来使うだろうとパディングを入れてあったけれど使わないことになった部分をカットして、という程度の場合がほとんどのようです
ちょっと横道にそれますが、いくつかのHackでは直接アクセスする必要が出てくる[UIGlobals]構造体もほとんど変更されていません もし、[UIGlobals]構造体にアクセスしたい場合には、OS5用に作ったヘッダファイルを差し上げますので催促してください
OS5対応Hackのお話はこれでおしまい
そのうち機会があればYAHMに頼らずに自前でHACKを管理する方法や「擬似」OS5ネイティブアプリケーションの作り方などをお話できればと思っています
質問、感想などはPalmHackersSalonのBBSへ