[7] ARM ARMは、根拠のクラッシュスタックのコンパイルと機能アセンブラ

オリジナル住所:

https://www.jianshu.com/p/f3f771f8f65b

オリジナルリンク  https://azeria-labs.com/functions-and-the-stack-part-7/

 

このセクションでは、目的と関連する操作を説明するためのスタックを、スタックと呼ばれるメモリのユニークなエリアを検討します。また、当社は、規則のARMアーキテクチャの関数を呼び出す検討します。

スタック

一般的に、スタックは、プログラム/プロセスのメモリの領域です。プロセスが作成されたときに、このメモリが作成されます。私たちは、そのようなローカル変数、および環境変数として一時的にデータを保存するためにスタックを使用しています。前回の記事では、スタック関連命令PUSHとPOPの動作について話しました。

前に私たちは始める、または知識とその実装スタック、それについて学びます。我々はそれが変化したときに、スタック上のデータの32を入れている成長のスタック、約最初の話。(スタックが実装されたときとき負の成長)スタックが高くなる可能性が上向き、(スタックプラス成長が達成される)下方、または増加。スタックポインタが決定される次の32ビットに特定のデータは、それがより正確にSPレジスタによって決定され、配置されています。格納された(すなわち、最後の時間である)が、指している現在のデータであってもよいが、それは次の場所に格納することができます。スタック(フルスタック実装)に格納された時点の現在位置にSPデータは、SPが減少する場合は(スタックを降順)、または増加(上昇スタック)、動作を指すのコンテンツ。アイドル位置にSPポイントが一度操作データに(空のスタックを達成するために)場合、データが最初に格納することができ、その後、SPは、(スタックを降順)デクリメントされ、または増加(上昇スタック)。

画像

(それは程度です...)異なるスタックの実装が表す様々な状況で複数の命令をアクセスすることができます。

スタックタイプ プッシュ(ストレージ) ポッピング(ロード)
フル下降スタック(FD、フル降順) (下降動作前STMDBに相当)STMFD (操作後にインクリメントLDMに相当)LDMFD
フルスタック昇順(FA、フル・昇順) STMFA(STMIBに相当する、スケール操作をスライド前) LDMFA(LDMDA、操作後のダウンに相当)
降順に空のスタック(ED、空降順) StmEd(ダウン操作後STMDAに相当) (スケール操作を摺動する前に、LDMIBに相当)LDMED
昇順(EA、空昇順)で空のスタック (操作後にインクリメントSTMに相当)STMEA (下降動作前LDMDBに相当)LDMEA

私たちの例では、フル下降スタックを実現するために、スタックを使用しています。スタック関連の例で見てみましょう。

 

/* azeria@labs:~$ as stack.s -o stack.o && gcc stack.o -o stack && gdb stack */
.global main

main:
     mov   r0, #2  /* 设置R0 */
     push  {r0}    /* 将R0存在栈上 */
     mov   r0, #3  /* 修改R0 */
     pop   {r0}    /* 恢复R0为初始值 */
     bx    lr      /* 程序结束 */

最初に、データ・スタックの位置を示すアドレス0xbefff6f8にスタックポインタ。あなたは、いくつかの値を格納するために、現在の位置を見ることができます。

 

gef> x/1x $sp
0xbefff6f8: 0xb6fc7000

第1命令MOV後、スタックは変更されません。後の最初のSP値を実行中の唯一のPUSH命令はマイナス4バイトです。R0に格納された値は、SPによって指された場所に格納された後。今、私たちは、SPおよびそれらによって指された位置の値を見てください。

 

gef> x/x $sp
0xbefff6f4: 0x00000002

指令値の後R0が3に変更しました。我々は、次に、現在のスタックデータストアの場所への指示は、SP値R0に格納されているPOP、及びSP値+ 4点を行いました。Zの最終的な値は2 R0です。

 

gef> info registers r0
r0       0x2          2

(以下の図が示すスタックの最上位の下位アドレスの動的変化)

画像

スタックは、レジスタ状態になるまで、ローカル変数を格納するために使用されます。管理機能は、スタックフレームの概念を使用し統一するために、特定の領域のスタックフレームは、スタック内のデータを記憶する機能です。スタックフレームは関数の先頭に作成されます。スタックフレーム要素の下を指すスタックフレームポインタ(FP)は、フレームポインタは、バッファがスタックに関連するスタックフレームを適用され、決定されます。(そのベースから)スタックフレームは、典型的には、リターンアドレス(LRは、前前記)、層のスタックフレームポインタ機能、ならびにレジスタのいずれかを含む関数は、4つの以上のパラメータを必要とする場合、関数のパラメータ(記憶する必要)、ローカル変数などが挙げられます。スタックフレームは、多くのデータが含まれますが、これらのタイプの多くは、我々の前に学んだ前に。最後に、スタックフレームは関数の終了時に破壊されます。

図は、スタック(スタックデフォルト、フル下降スタック)のスタックフレーム内の位置の抽象的記述です。

画像

より具体的な例に次のスタックフレームそれを理解するには:

 

/* azeria@labs:~$ gcc func.c -o func && gdb func */
int main()
{
 int res = 0;
 int a = 1;
 int b = 2;
 res = max(a, b);
 return res;
}

int max(int a,int b)
{
 do_nothing();
 if(a<b)
 {
 return b;
 }
 else
 {
 return a;
 }
}
int do_nothing()
{
 return 0;
}

下のスクリーンショットでは、GDBのスタックフレームに関連した情報を見ることができます:

画像

可以看到上面的图片中我们即将离开函数max(最下面的反汇编中可以看到)。在此时,FP(R11)寄存器指向的0xbefff254就是当前栈帧的底部。这个地址对应的栈上(绿色地址区域)位置存储着0x00010418这个返回地址(LR)。再往上看4字节是0xbefff26c。可以看到这个值是上层函数的栈帧指针。在0xbefff24c和0xbefff248的0x1和0x2是函数max执行时产生的局部变量。所以栈帧包含着我们之前说过的LR,FP以及两个局部变量。

函数

在开始学习ARM下的函数前,我们需要先明白一个函数的结构:

  1. 序言准备(Prologue)
  2. 函数体
  3. 结束收尾(Epilogue)

序言的目的是为了保存之前程序的执行状态(通过存储LR以及R11到栈上)以及设定栈以及局部函数变量。这些的步骤的实现可能根据编译器的不同有差异。通常来说是用PUSH/ADD/SUB这些指令。举个例子:

 

push   {r11, lr}    /* 保存R11与LR */
add    r11, sp, #4  /* 设置栈帧底部,PUSH两个寄存器,SP加4后指向栈帧底部元素 */
sub    sp, sp, #16  /* 在栈上申请相应空间 */

函数体部分就是函数本身要完成的任务了。这部分包括了函数自身的指令,或者跳转到其它函数等。下面这个是函数体的例子。

 

mov    r0, #1       /* 设置局部变量(a=1),同时也是为函数max准备参数a */
mov    r1, #2       /* 设置局部变量(b=2),同时也是为函数max准备参数b */
bl     max          /* 分支跳转调用函数max */

上面的代码也展示了调用函数前需要如何准备局部变量,以为函数调用设定参数。一般情况下,前四个参数通过R0-R3来传递,而多出来的参数则需要通过栈来传递了。函数调用结束后,返回值存放在R0寄存器中。所以不管max函数如何运作,我们都可以通过R0来得知返回值。而且当返回值位64位值时,使用的是R0与R1寄存器一同存储64位的值。

函数的最后一部分即结束收尾,这一部分主要是用来恢复程序寄存器以及回到函数调用发生之前的状态。我们需要先恢复SP栈指针,这个可以通过之前保存的栈帧指针寄存器外加一些加减操作做到(保证回到FP,LR的出栈位置)。而当我们重新调整了栈指针后,我们就可以通过出栈操作恢复之前保存的寄存器的值。基于函数类型的不同,POP指令有可能是结束收尾的最后一条指令。然而,在恢复后我们可能还需要通过BX指令离开函数。一个收尾的样例代码是这样的。

 

sub    sp, r11, #4  /* 收尾操作开始,调整栈指针,有两个寄存器要POP,所以从栈帧底部元素再减4 */
pop    {r11, pc}    /* 收尾操作结束。恢复之前函数的栈帧指针,以及通过之前保存的LR来恢复PC。 */

总结一下:

  1. 序言设定函数环境
  2. 函数体实现函数逻辑功能,将结果存到R0
  3. 收尾恢复程序状态,回到调用发生的地方。

关于函数,有一个关键点我们要知道,函数的类型分为叶函数以及非叶函数。叶函数是指函数中没有分支跳转到其他函数指令的函数。非叶函数指包含有跳转到其他函数的分支跳转指令的函数。这两种函数的实现都很类似,当然也有一些小不同。这里我们举个例子来分析一下:

 

/* azeria@labs:~$ as func.s -o func.o && gcc func.o -o func && gdb func */
.global main

main:
    push   {r11, lr}    /* Start of the prologue. Saving Frame Pointer and LR onto the stack */
    add    r11, sp, #4  /* Setting up the bottom of the stack frame */
    sub    sp, sp, #16  /* End of the prologue. Allocating some buffer on the stack */
    mov    r0, #1       /* setting up local variables (a=1). This also serves as setting up the first parameter for the max function */
    mov    r1, #2       /* setting up local variables (b=2). This also serves as setting up the second parameter for the max function */
    bl     max          /* Calling/branching to function max */
    sub    sp, r11, #4  /* Start of the epilogue. Readjusting the Stack Pointer */
    pop    {r11, pc}    /* End of the epilogue. Restoring Frame pointer from the stack, jumping to previously saved LR via direct load into PC */

max:
    push   {r11}        /* Start of the prologue. Saving Frame Pointer onto the stack */
    add    r11, sp, #0  /* 设置栈帧底部,PUSH一个寄存器,SP加0后指向栈帧底部元素 */
    sub    sp, sp, #12  /* End of the prologue. Allocating some buffer on the stack */
    cmp    r0, r1       /* Implementation of if(a<b) */
    movlt  r0, r1       /* if r0 was lower than r1, store r1 into r0 */
    add    sp, r11, #0  /* 收尾操作开始,调整栈指针,有一个寄存器要POP,所以从栈帧底部元素再减0 */
    pop    {r11}        /* restoring frame pointer */
    bx     lr           /* End of the epilogue. Jumping back to main via LR register */

上面的函数main以及max函数,一个是非叶函数另一个是叶函数。就像之前说的非叶函数中有分支跳转到其他函数的逻辑,函数max中没有在函数体逻辑中包含有这类代码,所以是叶函数。

除此之外还有一点不同是两类函数序言与收尾的实现是有差异的。来看看下面这段代码,是关于叶函数与非叶函数的序言部分的差异的:

 

/* A prologue of a non-leaf function */
push   {r11, lr}    /* Start of the prologue. Saving Frame Pointer and LR onto the stack */
add    r11, sp, #4  /* Setting up the bottom of the stack frame */
sub    sp, sp, #16  /* End of the prologue. Allocating some buffer on the stack */

/* A prologue of a leaf function */
push   {r11}        /* Start of the prologue. Saving Frame Pointer onto the stack */
add    r11, sp, #0  /* Setting up the bottom of the stack frame */
sub    sp, sp, #12  /* End of the prologue. Allocating some buffer on the stack */

一个主要的差异是,非叶函数需要在栈上保存更多的寄存器,这是由于非叶函数的本质决定的,因为在执行时LR寄存器会被修改,所以需要保存LR寄存器以便之后恢复。当然如果有必要也可以在序言期保存更多的寄存器。

下面这段代码可以看到,叶函数与非叶函数在收尾时的差异主要是在于,叶函数的结尾直接通过LR中的值跳转回去就好,而非叶函数需要先通过POP恢复LR寄存器,再进行分支跳转。

 

/* An epilogue of a leaf function */
add    sp, r11, #0  /* Start of the epilogue. Readjusting the Stack Pointer */
pop    {r11}        /* restoring frame pointer */
bx     lr           /* End of the epilogue. Jumping back to main via LR register */

/* An epilogue of a non-leaf function */
sub    sp, r11, #4  /* Start of the epilogue. Readjusting the Stack Pointer */
pop    {r11, pc}    /* End of the epilogue. Restoring Frame pointer from the stack, jumping to previously saved LR via direct load into PC */

最后,我们要再次强调一下在函数中BL和BX指令的使用。在我们的示例中,通过使用BL指令跳转到叶函数中。在汇编代码中我们使用了标签,在编译过程中,标签被转换为对应的内存地址。在跳转到对应位置之前,BL会将下一条指令的地址存储到LR寄存器中这样我们就能在函数max完成的时候返回了。

BX指令在被用在我们离开一个叶函数时,使用LR作为寄存器参数。刚刚说了LR存放着函数调用返回后下一条指令的地址。由于叶函数不会在执行时修改LR寄存器,所以就可以通过LR寄存器跳转返回到main函数了。同样BX指令还会帮助我们切换ARM/Thumb模式。同样这也通过LR寄存器的最低比特位来完成,0代表ARM模式,1代表Thumb模式。

最后,这张动图阐述了非叶函数调用叶函数时候的内部寄存器的工作状态。



作者:Arnow117
链接:https://www.jianshu.com/p/f3f771f8f65b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

 

 

下面自己来总结一下几个寄存器:

PC: 指向当前执行指令的地址

SP 指向当前栈的栈顶位置。程序运行会开辟一个栈空间。然后在每一个函数,会在栈内开辟栈帧

P11/FP: 指向当前栈帧的底部。在arm中是向上申请空间,所以申请到空间的地址编号是越来越小的。也就是说栈帧底部是当前栈帧中最大的地址(在函数创建的时候,就会对应生成一个栈帧,用来存放函数中的所有数据)

LR:保存返回地址。就是本函数执行完之后要跳转的位置。

接下来看几个函数执行的真实例子

########################################################################################################################################################################################################

例1:

这是我们的一个子函数mfun1.。main函数掉员工子函数。main函数跟这里无关,就不贴出来了。但是main函数中设置了自己的SP和R11.

这里我们为每一条指令标了号。如图:1-17 。这里一共是17条指令:

下面来介绍下每条指令的意思:

 

SP 是main函数的数栈的栈顶。R11是main函数函数栈的栈底。这里在本函数调用前就确定了。

1、保存main函数栈底。这里R11是main函数的栈底。需要保存到SP - 4 的位置(FF9AE268-4 = FF9AE246).同时注意后面感叹哈号。需要更改SP寄存器为 FF9AE246

2、设置当前的函数的栈底R11位置。这里就直接是SP + 0 。也就是本函数的栈底为 FF9AE246

3、这里做个减法其实是在开辟栈帧的空间。这个空间就是函数特有的自己的空间。0xC就是十进制12.就是开辟了12字节的空间。3*4  = 12 这里有3个参数。后面可以看到a,b,c三个参数都占了4个字节。这时候SP 指向的是FF9AE258 这个位置。

如此;函数的初始化完成。指令4开始是函数的逻辑。这里不予介绍。此时栈的状态。如下:

大致的可以看到:

 

FF9AE258(fun1 函数 SP指向的位置)

......(这一块是func1函数的栈)

FF9AE264(fun1 函数 R11指向的位置)

FF9AE268(main 函数 SP指向的位置)

......(这一块是main函数的栈)

FF9AE27C(main 函数 R11指向的位置)

 

程序持续运行运行完成 指令14    mov R0,R3.

这里其实就已经完成函数的所有逻辑了。R0中保存着函数的返回值。

此时我们再来看看栈的状态。

main函数栈的部分没有改动。当前的函数中保存的main函数R11也还在。

接下来15、16 指令就是完成栈的回收工作了。

15、执行完之后。SP 和 R11相等了。这里都等于 FF9AE264。

也就是说

FF9AE258(fun1 函数 SP指向的位置)

......(这一块是func1函数的栈)

FF9AE264(fun1 函数 R11指向的位置)

这一块空间被释放了。

16、这里是设置R11为FF9AE260 。一个新为位置。最后是函数BX返回。

 

########################################################################################################################################################################################################

例2 

这个跟前面相比多了参数,以及函数的内部还有调用另外函数

来看看我们的执行流程吧。

同样的是main函数调用 fun2。所以在调用fun2前。mian函数的SP和R11已经确定了。

同样是

FF9AE268(main 函数 SP 指向的位置)

......(这一块是main函数的栈)

FF9AE27C(main 函数 R11指向的位置)

此时的栈

接下来执行

指令1      STMFD   SP!, {R4,R11,LR}

就是将main函数的

LR   ---> F773E4F0

R11 ---> FF9AE27C

R4 --->  FF9AE2BC

依次都保存到栈中。

同时注意感叹号。这里的SP 函数栈顶变成  FF9AE25C

这里相比之前多了保存LR是因为这里的fun2中调用了其他的函数。保存LR是后面返回的时候需要用到。LR这里的F773E4F0 其实是指向main中的下一条指令。也就是fun2 之后返回需要执行的第一条指令的位置。

执行指令 2

ADD     R11, SP, #8

这里是设置函数栈底R11.此时的 SP 函数栈顶变成  FF9AE25C。因为前面入栈了 3*4 = 12 字节的数据。

这里用一个加法 就是要让SP指针往下游走。8 是游走8个字节。如何确定常熟8?看前一条指令。STMFD 这个指令入栈了3个寄存器的值。每个寄存器里面有4字节数据。所以是入栈了12字节的值。

main指令的栈顶指向的 FF9AE268

然后入栈了3个 寄存器的值。我们的函数栈底需要设置在  FF9AE268  - 4  = FF9AE264的位置上。

于是后可以这么算 (入栈寄存器的数量 -1) *4  就是栈底的偏移位置。

这里就是(3-1)* 4 = 8 

执行完成后。毫无疑问 R11 就是新函数栈底。设置在 FF9AE264 位置上.

指令3   SUB     SP, SP, #0xC

同样的设置了栈底之后。需要蛇者栈。也就是开辟栈的空间。#0xC 也就是十进制12.字节。所以SP 此时指向FF9AE250

于是fun2的初始化完成了。此时大致可以看到

FF9AE250(fun2 函数 SP指向的位置)

......(这一块是func2函数的栈)

FF9AE264(fun2 函数 R11指向的位置)

FF9AE268(main 函数 SP指向的位置) = 

......(这一块是main函数的栈)

FF9AE27C(main 函数 R11指向的位置)

接下去就是函数的逻辑内容。这里不详细分析。

要注意的是。因为函数中调用了其他函数。所以LR 是会更更改的。

执行到指令 12  MOV     R0, R3  这里得到函数fun2的返回值。意味着函数fun2的逻辑正式运行完成。接下来就是fun2的函数空间释放问题。

指令13  SUB     SP, R11, #8

这条指令是跟 指令2  ADD     R11, SP, #8 相对应的。

R11 是fun2 的栈底指针。当前是FF9AE264  .这里就是设置SP 为  FF9AE264 - 8  = FF9AE25C位置上。移到这里的以为也很明显。就是反着走初始化的流程。将SP指向上层main函数的sp栈顶指针+ 12字节为位置。

指令14    LDMFD   SP!, {R4,R11,PC}

这个指令很明显是接着指令13执行的。上面说到SP指针的移动。特意补上了8个字节。就因为要执行指令14。要出栈3个寄存器的值。刚好就是12个字节。

于是执行这条指令。sp的值增加12字节。最后指向main 函数的SP指针。也就是  FF9AE268 的位置。

至此。func2 函数执行完成,栈帧数据也成功释放出去。

########################################################################################################################################################################################################

例3

这个函数相对于上面两个例子来说,不仅在内部调用其他函数。而且还有超过4个的参数。在arm中,少于4个参数的函数会默认使用寄存器R0,R1,R2,R3 来传递参数。但是如果函数的参数多于4个,就需要使用栈帧来传递函数了。这样在函数的执行前不仅需要将上层调用函数的栈帧底部R11,LR等寄存器压入栈帧中,还需要将函数参数也压入栈帧中。不过这里有个需要注意的地方。参数是在main中进行传递的。因此,是压入main函数的栈帧中。

在main中,如果我们这样调用fun3:

fun3(1,2,3,4,5,6);

那么对应的fun3的arm代码应该是这样的:

这里通过R0-R3 依次传递前面4个参数。后面的两个参数就要依靠栈来传递。比如这里运行到指令9时。栈的状态如下:

这里main 函数的栈底R11指向的是 FFDB164C ,栈顶SP指向的是的是FFDB1638。R11根SP之间的内容就是main用到的空间。

这里注意下FFDB1638 保存了一个数值5,FFDB163C保存了数值6。这其实就是我们调用fun3需要的第5,第6个常数。这里已经压入main 的栈中了。 另外可以看到,R0-R3也依次存放着前面4个参数的值。

我们再来看看fun3 的arm 代码:

执行指令1  STMFD   SP!, {R11,LR}

同样将main 的LR,R11压入栈中。这里的R11 是  FDB164C,  LR指向的是main中fun3执行返回后的第一条指令。也就是F70FC514的位置。注意入栈之后的SP是指向现在的  FFDB1630位置。

执行指令2   ADD     R11, SP, #4

因为指令1入栈了2个寄存器。所以是压入了2*4 = 8个字节。这里R11是要指向当前fun3函数栈底的。也就是FFDB1634 的位置。所以这里要ADD  4。

指令3   SUB     SP, SP, #0x18

这里的0x18也就是十进制的24,也就是说还要开辟24个字节的空间。 4*6 = 24,所以SP的位置在 FFDB1630 基础上还要往上走24. 。视图中往上蹦6层

至此。我们新函数的函数栈就搭建起来了。

FFDB1638 ~  FFDB164C  之间是main函数的空间。

FFDB1638 同时也是栈顶SP指向的位置。 FFDB164C 是栈底R11指向的位置

栈顶的 FFDB1638 ,FFDB163C位置上依旧保存着main函数需要传递到fun3中的参数5,和参数6. 

FFDB1634 ~  FFDB1618  之间是fun3函数的空间。

该空间由两次操作创建的。第一次是 STMFD 的入栈。入栈了2个寄存器,8字节大小。第二次是SUB 指令修改SP指针位置创建,开辟了24字节大小。所以一共就是32(4*8,视图中一共8层)字节。

至此,初始化工作完成,接下来执行函数逻辑。这里不关注中间逻辑直接跳到

k=  4
j=  8

指令28   LDR     R3, [R11,#k]   

指令30    LDR     R3, [R11,#j] 

关注这两条指令是因为这两条指令是从main 函数栈中取出第5,第6个参数。这里的R11 是指向fun3函数的 栈底。要访问到main函数的的内容。需要R11往下走。这里偏移4,偏移8 刚好就是第5,6个参数的位置。

指令33   SUB     SP, R11, #4

这里是将fun3函数的栈底R11往上走4个字节。这是与前面的  ADD     R11, SP, #4 相对应的。执行完成之后。sp会指向到FFDB1630位置。刚好也就是将 SUB     SP, SP, #0x18 这个指令 申请的24 字节的空间释放出去了。

最后是指令 LDMFD   SP!, {R11,PC}

这个指令将  FFDB1630  和 FFDB1634 这8个字节的的空间释放出去。至此。fun3 的函数的所有工作执行完成,然后返回到main函数。返回的位置就是最先设置的LR寄存器,也就是F7482514 的地方。

 

 

 

 

发布了24 篇原创文章 · 获赞 10 · 访问量 16万+

おすすめ

転載: blog.csdn.net/lin___/article/details/103816918