IDA proに新しいプロセッサフ​​ァミリを追加する

少し前まで、M16C(Mitsubishi / Renesas)からファームウェアを削除する必要がありました。 IDA v6.1.xxxがこのファミリーのコントローラーを「保持」しないことが判明したことに驚いた。 ただし、SDKは利用可能です。つまり、怖くないということではありません。状況を修正します。 実践が示しているように、モジュールを書くことの複雑さ以外には何もありません(ロケット科学、お茶ではありません)。



免責事項



私はIDA proのスペシャリストではなく、そのためのモジュールを作成しています。 タスクはファームウェアを分析することであったため、モジュールは急いで(ひざまずく)書かれ、コードの一部はSDKから引き出されましたが、動作方法(およびこれが必要かどうか)を理解していませんでした。 残りのコードは整理され、「創造的に」再考されました。



テストも書く時間もありませんでした。 作業の正確性は、Renesas IDEの逆アセンブルされたファームウェアのリストで確認されました。 したがって、エラーが発生する可能性があります(プロセス中にエラーに遭遇することはありませんでしたが、確かにあります)。 誰かがテストを書いたり、モジュールを変更したいという願望を持っているなら、私はうれしいです。



それはそうかもしれないが、モジュールは非常に機能していることが判明し、タスクを完了することができた。 この記事では、これらすべてに対する私の考えを概説します。 したがって、この作業は自己責任で行ってください。



ソースとプラグインアセンブリ



ソースはこちらです。

私は特にStudio(MSVC ++)を尊重していないため、アセンブリにMinGWを使用し、ツールチェーンを積み重ねました。 ここで取ることができます 。 このツールチェーンは自給自足です-コンパイラとアセンブリ用のツールが含まれています。



組み立ての準備は次のとおりです。 IDA_Plugins.7zをどこかで解凍し、GitHubからリポジトリを複製し、m16c_xxディレクトリをIDA_Pluginsディレクトリのルートにコピーしてから、build.cmdを実行します。



はじめに



実際には通常のdllである各プロセッサモジュール(わずかな違いはありますが、DOSヘッダーは若干変更されています)は、タイプprocessor_tのLPHという名前の構造体をエクスポートする必要があります。 主な機能とモジュール構造へのポインタを保存します。

この構造のすべてのさまざまなフィールドのうち、主に次の関数ポインターに関心があります。



processor_t LPH = { … ana, // analyze an instruction and fill the 'cmd' structure emu, // emulate an instruction out, // generate a text representation of an instruction outop, // generate a text representation of an operand … }
      
      





彼らの助けを借りて、基本的に、すべての作業は完了です。 ana()関数は、新しい命令を分析するときに毎回(分析するものがある場合)、そのシグネチャを呼び出します。



 int idaapi ana(void); //analyze one instruction and return the // instruction length
      
      





この関数のタスクは、現在の命令アドレスポインターからバイトを選択することにより、命令のデコードを試みます。うまくいかない場合は、命令全体とそのオペランドがデコードされるまで後続のバイトを選択し続けます。 次に、グローバル変数cmdのフィールドに入力し、命令の長さをバイト単位で返します。



emu()関数は、命令、その署名をエミュレートすることを目的としています。



 int idaapi emu(void); //emulate one instruction
      
      





この機能の目的は次のとおりです。



out()関数は、アセンブラー命令のテキスト表現、その署名を作成して表示します。



 void idaapi out(void); //output a single disassembled instruction
      
      





outop()関数は、アセンブラー命令のオペランド、その署名のテキスト表現を作成して表示します。



 bool idaapi outop(op_t &x); //output an operand of disassembled // instruction
      
      





命令分析



ソースデータの分析は、実装する必要がある関数ana()によって実行されます。 ファームウェアのバイトを順番に読み取るタスクは、命令、そのオペランド、および命令の長さを決定します。



命令とそのパラメーターを認識した後、 insn_t型のグローバル変数cmdのフィールドに入力します



 class insn_t { public: ea_t cs; // Current segment base paragraph. Set by kernel ea_t ip; // Virtual address of instruction (within segment). // Set by kernel ea_t ea; // Linear address of the instruction. Set by kernel uint16 itype; // instruction enum value (not opcode!). // Proc sets this in ana uint16 size; // Size of instruction in bytes. Proc sets this in ana union { // processor dependent field. Proc may set this uint16 auxpref; struct { uchar low; uchar high; } auxpref_chars; }; char segpref; // processor dependent field. Proc may set this char insnpref; // processor dependent field. Proc may set this op_t Operands[6]; // instruction operand info. Proc sets this in ana char flags; // instruction flags. Proc may set this };
      
      





説明からわかるように、命令には最大6つのオペランドを含めることができます(この場合、「背後」にあります-この場合、操作には最大3つのオペランドが含まれます)。



これまでのところ、RISCコントローラー専用のモジュールを作成しました。すべてが非常にシンプルです。 命令のCPCフィールド(オペレーションコード)にマスクを配置し、 switchステートメントの分岐で追加条件確認します。実際、すべて(Microchip PICコントローラーのコードはほぼこの方法で分析されます。例としてSDKを見ることができます)。 ここで、RISCの利点は明らかです。コマンドのセットが減り、長さが等しくなります。 残念ながら、M16Cについては300を超える一意のコマンド(すべては視点に依存します-一意のCPCを考慮しました)、さらに可変長コマンド(CISC)をカウントしました。 したがって、 switchステートメントは、キャッチしにくいエラーの扱いやすさと弱さのために、私たちには適していません。



ここで、少し余談をする必要があります。 マシンコード(およびアセンブラ自体)は実際には通常の言語文法 )であるため、本質的にはプロセッサであるステートマシン(FSM)を使用して問題なく理解する必要があります。



マシンを手動で実行したいという希望はありませんでしたが、C、C ++、C#、Objective-C、D、Java、OCaml、Go、またはRubyのコードで、特別なFSM記述言語からの有限状態マシンのコンパイラであるRagelには肯定的な経験がありました。 外部依存関係は作成されず、選択されたプログラミング言語の自給自足のソースのみが作成されます。



とりわけ、 Ragelは入力データをその場で解析できるという点で興味深いです。 つまり コマンドを含むことが保証されている大きなバッファーを分析のためにパーサーに形成して渡す必要はありませんが、呼び出し間の状態を維持しながら、最大1バイトの少量のデータに制限することができます。 私たちにぴったりです!



結果は、プロセッサ命令を解析するための一種のDSLです。



コマンドを解析するためのDSL



スイッチに対するこのDSLの利点は、主にその直線性です。 これがどのように機能するかを理解したり、動作を変更したりするために、演算子の分岐をスキップする必要はありません。 特定のコマンドのCPCとオペランドのすべての処理は、1箇所に集中しています。 例:

 #// 0x00 0000 0000 BRK M16C_BRK = 0x00 @ { cmd.itype = M16C_xx_BRK; cmd.Op1.type = o_void; cmd.Op1.dtyp = dt_void; };
      
      





または、オプションとして、オペランドを指定したコマンド:



 #// 0x01..0x03 0000 00DS MOV.B:S R0L, DEST M16C_MOV_B_S_R0L_DEST = (0x01..0x03) @ { cmd.itype = M16C_xx_MOV_B_S_R0L_DEST; MakeSrcDest8(SRC_DEST_R0L, cmd.Op1); switch(*p & 0x03) { case 0x01: MakeSrcDest8(SRC_DEST_DSP_8_SB_, cmd.Op2); break; case 0x02: MakeSrcDest8(SRC_DEST_DSP_8_FB_, cmd.Op2); break; default: MakeSrcDest8(SRC_DEST_ABS16, cmd.Op2); break; } };
      
      





それは非常に明確で便利なように思えます。 enum opcodes列挙値の1つ( ins.hppファイル)はcmd.itype配置されます 。これは、命令のテキスト表現、オペランドの数を示し、命令とオペランドの相互作用をさらに説明します。 また、オペランドフィールドが入力されます。



命令実行エミュレーション



命令自体、オペランドの数、およびオペランドへの影響は、 instruc_t命令[] 配列ins.cpp )で説明されています。 基本的に、レコード形式はシンプルで直感的です。



 instruc_t instructions[ ] = { ... { "ADC.B", CF_USE1|CF_CHG2 }, { "ADC.W", CF_USE1|CF_CHG2 }, { "ADC.B", CF_USE1|CF_CHG2 }, { "ADC.W", CF_USE1|CF_CHG2 }, { "ADCF.B", CF_CHG1 }, { "ADCF.W", CF_CHG1 }, { "ADD.B:G", CF_USE1|CF_CHG2 }, { "ADD.W:G", CF_USE1|CF_CHG2 }, { "ADD.B:Q", CF_USE1|CF_CHG2 }, ... };
      
      





命令「 ADC.B 」には2つのオペランドがあり、最初のオペランドのみが使用され、2番目のオペランドは命令の実行中に変更されることがわかります。 これは論理的です。ADCキャリー付きのADditionであり、操作は次のようになります。



 [ Syntax ] ADC.size src,dest ^--- B, W [ Operation ] dest <- src + dest + C
      
      





次に、 emu()関数での命令自体の実行がエミュレートされます。



 int emu( ) { unsigned long feature = cmd.get_canon_feature( ); if( feature & CF_USE1 ) TouchArg( cmd.Op1, 1 ); if( feature & CF_USE2 ) TouchArg( cmd.Op2, 1 ); if( feature & CF_CHG1 ) TouchArg( cmd.Op1, 0 ); if( feature & CF_CHG2 ) TouchArg( cmd.Op2, 0 ); if( !( feature & CF_STOP ) ) ua_add_cref( 0, cmd.ea + cmd.size, fl_F); return 1; }
      
      





ご覧のとおり、引数を使用して、 TouchArg()関数でそれを消化可能な形式に変換します。 この関数は次のようになります。



 void TouchArg( op_t &x, int isload ) { switch ( x.type ) { case o_near: { cref_t ftype = fl_JN; ea_t ea = toEA(cmd.cs, x.addr); if ( InstrIsSet(cmd.itype, CF_CALL) ) { if ( !func_does_return(ea) ) flow = false; ftype = fl_CN; } ua_add_cref(x.offb, ea, ftype); } break; case o_imm: if ( !isload ) break; op_num(cmd.ea, xn); if ( isOff(uFlag, xn) ) ua_add_off_drefs2(x, dr_O, OOF_SIGNED); break; case o_displ: if(x.dtyp == dt_byte) op_dec(cmd.ea, xn); break; case o_mem: { ea_t ea = toEA( dataSeg( ),x.addr ); ua_dodata2( x.offb, ea, x.dtyp ); if ( !isload ) doVar( ea ); ua_add_dref( x.offb, ea, isload ? dr_R : dr_W ); } break; default: break; } }
      
      





オペランドのタイプに応じて、 op_t構造体のフィールドに適宜入力します(オペランドを「デコード」します)。



命令のテキスト表現を出力します



out()関数がこのアクションを担当します。 次のようになります。



 void out() { char str[MAXSTR]; //MAXSTR is an IDA define from pro.h init_output_buffer(str, sizeof(str)); OutMnem(12); //first we output the mnemonic if( cmd.Op1.type != o_void ) //then there is an argument to print out_one_operand( 0 ); if( cmd.Op2.type != o_void ) { //then there is an argument to print out_symbol(','); out_symbol(' '); out_one_operand( 1 ); } if( cmd.Op3.type != o_void ) { //then there is an argument to print out_symbol(','); out_symbol(' '); out_one_operand( 2 ); } term_output_buffer(); gl_comm = 1; //we want comments! MakeLine(str); //output the line with default indentation }
      
      





最小限の書式設定で、命令のテキスト表現と、もしあればオペランドを印刷します。



オペランドの出力テキスト表現



ここでコードはより興味深いですが、すべてが非常に簡単です:



 bool idaapi outop(op_t &x) { ea_t ea; switch (x.type) { case o_void: return 0; case o_imm: OutValue(x, OOF_NUMBER | OOF_SIGNED | OOFW_IMM); break; case o_displ: { //then there is an argument to print OutValue(x, OOF_NUMBER | OOF_SIGNED | OOFW_IMM); switch (x.dtyp) { case dt_byte: break; case dt_word: out_symbol(':'); out_symbol('8'); break; case dt_dword: out_symbol(':'); out_symbol('1'); out_symbol('6'); break; default: ea = toEA(cmd.cs, x.addr); if (!out_name_expr(x, ea, x.addr)) out_bad_address(x.addr); break; } out_symbol('['); out_register(M16C_xx_RegNames[x.reg]); out_symbol(']'); } break; case o_phrase: out_symbol('['); out_register(M16C_xx_RegNames[x.reg]); out_symbol(']'); break; case o_near: ea = toEA(cmd.cs, x.addr); if (!out_name_expr(x, ea, x.addr)) out_bad_address(x.addr); break; case o_mem: ea = toEA(dataSeg(), x.addr); if (!out_name_expr(x, ea, x.addr)) out_bad_address(x.addr); break; case o_reg: out_register(M16C_xx_RegNames[x.reg]); break; default: warning("out: %a: bad optype %d", cmd.ea, x.type); break; } return true; }
      
      





オペランドのタイプに応じて、画面への出力を適切に配置します。



特定された欠陥と問題



現時点では、1つの重大な誤解があります。 未定義データに文字列(ASCII-Z文字列)を作成しようとすると、ゼロバイトがまだ検出されていない場合でも、4バイトの倍数のアドレスまでのみ文字列が作成されます。 ゼロバイトが先に発生した場合、行はそれで終了します。 アレイでは、同じ問題。



最も不愉快なのは、この状況でどこを掘ればよいかさえわからないということです。 誰か教えてもらえますか?



おわりに



したがって、IDA proのプラグインを作成することはそれほど難しくありません。 ターゲットアセンブラの多数のコマンドを除き、すべてが単純で、かなり面倒です。 CPCおよびオペランドを解析するためのDSLの導入により、開発が大幅に簡素化および高速化されます。



All Articles