C コード例では ARM32 アセンブリを詳細に説明しています

著者は Android セキュリティの分野に焦点を当てています。私の個人 WeChat 公開アカウント「 Android Security Engineering 」にご注目ください(クリックしてコードをスキャンしてください)。個人の WeChat パブリック アカウントは主に Android アプリケーションのセキュリティ保護と逆分析に焦点を当てており、さまざまなセキュリティ攻撃と防御方法、フック テクノロジー、ARM コンパイル、その他の Android 関連の知識を共有しています。私の個人的な WeChat をフォローして、Android セキュリティ巨人の台頭を目撃してください~

二分探索アルゴリズムの例

C 言語で二分探索アルゴリズムを実装する関数の例は次のとおりです。

int binarySearch(int arr[], int left, int right, int target) {
    
    
    while (left <= right) {
    
    
        int mid = (left + right) / 2;
        
        if (arr[mid] == target) {
    
    
            return mid;
        } else if (arr[mid] < target) {
    
    
            left = mid + 1;
        } else {
    
    
            right = mid - 1;
        }
    }
    return -1;
}

この関数は、配列、検索する左境界と右境界、および検索するターゲット要素を受け取ります。この関数では、まず、leftandによってright配列の中央の要素を決定しmid、それをターゲット要素と比較します。等しい場合は、中央の要素のインデックスを返します。中央の要素がターゲット要素より小さい場合は右側で検索され、そうでない場合は左側で検索されます。ターゲット要素が見つからない場合は、-1見つからなかったことを返します。

ARM32 アセンブリ分析に目を向ける

C コードを ARM32 アセンブリ コードに変換した後の関数は次のとおりです。

binarySearch:
    @ r0 = arr, r1 = left, r2 = right, r3 = target
    push    {r4, lr}        @ 保存 r4 和 lr 寄存器
    mov     r4, r1          @ 将左边界保存到 r4 中
loop:
    cmp     r1, r2          @ 比较左右边界
    bgt     end             @ 如果左边界大于右边界,则跳转到 end 标签
    add     r5, r1, r2      @ 计算中间位置
    mov     r5, r5, asr #1
    ldr     r6, [r0, r5]    @ 将中间位置的元素加载到 r6 中
    cmp     r6, r3          @ 比较中间位置的元素和目标元素
    beq     found           @ 如果相等,则跳转到 found 标签
    bge     right           @ 如果中间位置的元素小于目标元素,则跳转到 right 标签
left:
    add     r1, r5, #1      @ 计算下一次查找的左边界
    b       loop            @ 跳转到 loop 标签
right:
    sub     r2, r5, #1      @ 计算下一次查找的右边界
    b       loop            @ 跳转到 loop 标签
found:
    mov     r0, r5          @ 将找到的位置保存到 r0 中
    pop     {r4, pc}        @ 恢复 r4 和 pc 寄存器,跳转到函数调用位置
end:
    mov     r0, #-1         @ 如果未找到,则将 r0 设置为 -1
    pop     {r4, pc}        @ 恢复 r4 和 pc 寄存器,跳转到函数调用位置

上記のアセンブリ コードのコメントは、アセンブリ命令の各行の機能を詳細に説明しています。アセンブリ コードに触れたことのある読者は、次のような疑問を持つと思います。push {r4, lr} このアセンブリ ラインの具体的な機能は何ですか?

push {r4, lr}の役割は、レジスタの値を現在のスタック フレームにr4保存することです。lr通常、この命令は、関数が呼び出されたときに現在の関数スタック シーンを保存するために使用されpush、呼び出されたサブ関数が戻ったときにシーンを復元できるようになります。

この関数では、r4レジスタは左境界の値を格納するために使用され、lrレジスタはリンク レジスタであり、リターン アドレスを格納するために使用されます (ここで記録されるアドレスは、現在のジャンプ関数の次の命令のアドレスです) 、つまりlr=pc+1)。したがって、これら 2 つのレジスタの値を保存するために を使用するのpush {r4, lr}は、関数の実行中にそれらの値が上書きされたり変更されたりしないようにするためです。

関数の実行後、関数から呼び出しpop元に戻るために、命令を使用してシーンを復元し、以前にスタックに保存された値を取り出し、それを レジスタ とr4レジスタlrに再度格納する必要があります。

この点を読んだ後、一部の読者は疑問を持つと思います。なぜ上記のアセンブリr5の と はr6スタックにプッシュされないのですか?

この関数において、r5およびr6レジスタは、中間位置と中間位置の要素値を計算して格納するために関数内でのみ使用される一時レジスタです。これらの値は関数の実行中に複数回変更される可能性がありますが、関数呼び出し全体で値を保持する必要はありません。

したがって、関数の先頭で、とレジスタの値をスタックにプッシュpush {r4, lr}する命令を使用します。これは、関数を呼び出す前にシーンを保存し、関数の実行終了後にシーンを確実に復元できるようにするためです。 。関数内の一時レジスタの場合、値を保存する必要がないため、値をスタックにプッシュする必要はありません。r4lr

他のレジスタが関数内の一時レジスタとして使用され、関数呼び出しの終了後にこれらのレジスタの値を保存する必要がある場合、これらのレジスタの値をスタックにプッシュする必要があることに注意してください。実行終了後にシーンを復元する関数に保存できるように、関数の先頭に追加します。

push命令とは、スタックにデータを書き込む操作であり、このプロセスは、単純にレジスタ (またはメモリ) からメモリ内のアドレスにデータを移動することとして理解できます。ARM32 アセンブリでは、スタックを使用して現場の情報を保存することができます。たとえば、呼び出された関数は、実行中に現在のレジスタ値、リターン アドレス、パラメータなどの情報を保存する必要があります。これらの情報は、値をプッシュすることで実現できます。レジスタのスタックへの格納。

ARM32 アーキテクチャでは、スタックの実装は通常、先頭ポインタ (スタック ポインタ、SP) をスタックの先頭のアドレスにポイントし、データを保存する必要があるときにデータをスタックにプッシュすることによって行われます。スタックのトップ ポインタはデクリメントされ、それ以外の場合、データをポップする必要がある場合は、スタックのトップ ポインタがインクリメントされ、データがスタックからポップされます。このように、スタックの先頭ポインタの位置を制御することで、スタックのプッシュ、ポップ動作を実現し、シーンの保存、復元を実現することができる。

具体的には、命令が実行されると、命令はレジスタ内の値とスタックに値を順番にpush {r4, lr}プッシュし、スタック トップ ポインタは8 バイト減分され (は両方とも 4 バイト長であるため)、スタックの新しい先頭アドレス。このようにして、オンサイト情報の保存が実現され、その後のコード実行の準備も整います。r4lrSPr4lr

シーンを復元する必要がある場合、pop通常、この命令を使用してスタック内のデータをレジスタにポップし、元の値を復元します。具体的には、pop {r4, lr}命令が実行されると、命令はスタックの先頭の値をレジスタ と にポップしlrr4スタック トップ ポインタ SP は 8 バイト増加して、スタックの新しい先頭アドレスを指します。これでシーン情報の復元が完了し、シーンを保存した場所から機能を継続して実行できるようになります。

アセンブリプッシュ命令はシーンを保持するために使用されているのに、なぜそれがスタック内でまだ使用できるのでしょうかr4?

ARM32 アセンブリ言語では、pushと のpop命令を使用すると、操作の保存と復元が簡単に行えます。これらの 2 つの命令は、スタック ポインタ (スタック ポインタ、SP) を自動的にデクリメントまたはインクリメントし、レジスタ内のデータをスタックにプッシュまたはポップするためです。関数を呼び出す場合、push通常、この命令は現在の関数の戻りアドレスとレジスタの値をスタックにプッシュするために使用され、その後、pop戻るときにシーン情報を元の状態に戻すために使用されます。

push命令はスタックにプッシュされるときに と レジスタの値のみを保存するため、他のレジスタの値を保存するためのスペースはスタック上に割り当てられないため、他のレジスタ ( や など) にはスペースが割り当てられr4ませ) この関数のスタック フレーム内にあります。実際、これらのレジスタをこの関数で使用する必要がある場合、その値はレジスタに直接保存でき、保存するためにスタックを使用する必要はありません。スタックに保存する必要があるレジスタ値のみを、および命令を通じて保存および復元する必要があります。lrr5r6pushpop

つまり、pushおよびpop命令を使用してシーンを保存および復元する場合は、その命令によって保存されるレジスタの数と順序、スタック ポインタの位置などの詳細に注意する必要があります。

さらにpush {r4, lr}、ARM アセンブリ言語には、シーンの保存に使用できる同様の命令が他にもあります。一般的な命令のいくつかを次に示します。

  1. stmdb sp!, {r4, lr}: レジスタの値を現在のスタック フレームに保存しr4lr関数がsp返されたときにスタック ポインタから対応する値を減算してシーンを復元します。
  2. stmfd sp!, {r4, lr}:stmdbと同じ機能で、レジスタの値を現在のスタック フレームに保存しますr4lrspスタック ポインタを対応する値だけ増加させます。
  3. sub sp, sp, #size:の値をsp減算して、サイズのスタック領域を開きます。たとえば、スタック上に 16 バイトのスペースを空けることを意味します。sizesizesub sp, sp, #16

補足紹介

ARM アセンブリ言語では、push命令とstm命令の両方を使用してコンテキストを保存できます。これらの使用方法とその効果にはいくつかの違いがあります。

  1. オペランド:push命令は連続するレジスタをスタックに順次プッシュすることしかできませんが、stm命令は保存する必要があるレジスタを指定できます。たとえば、およびレジスタの値をスタックに保存stmdb sp!, {r4, lr}できます。r4lr
  2. スタック ポインタの変更:push命令は保存するレジスタを指定するだけで、スタック ポインタのオフセットを自動的に計算し、値をスタックに保存します。stmレジスタの値を保存した後、スタック ポインタが次のスタック フレームを正しく指すことができるように、命令はスタック ポインタの変更量を明示的に指定する必要があります
  3. 即値データ:push命令は定数値をプッシュするために即値データを使用できますが、stm命令はレジスタ値を保存するためにのみ使用できます。
  4. 柔軟性:stm命令は命令よりも柔軟性がありpush、任意の数および順序のレジスタを格納するために使用できます。この命令は、push連続したレジスタを順次に保存することしかできません。

pushなお、この命令は連続したレジスタを順番に退避することしかできないため、退避するレジスタが連続していない場合には複数のpush命令を使用する必要があります。この命令はstm複数の不連続レジスタを一度に保存できるため、命令の数が減り、コードの効率が向上します。

おすすめ

転載: blog.csdn.net/HongHua_bai/article/details/129210554