Subroutine Call of x86-64

890阅读 0评论2021-09-04 静默梧桐
分类:LINUX

上一篇介绍了Arm64 FP/LR两个寄存器,并通过一个简单的例子来解释了它们在函数调用中作用。这里继续介绍x86的子函数调用及stact frame,其中用到同上篇的toy example来帮助理解所讲的内容。

Register

首先简单介绍下寄存器与栈帧,这个是理解subroutine call的基础。
来看一下几个与本节内容相关的x86-64 arch的寄存器

Register

Purpose

%rax

temp register; return value

%rbx

callee-saved

%rcx

used to pass 4th argument to functions

%rdx

used to pass 3rd argument to functions

%rsp

stack pointer

%rbp

callee-saved; base pointer

%rsi

used to pass 2nd argument to functions

%rdi

used to pass 1st argument to functions

%r8

used to pass 5th argument to functions

%r9

used to pass 6th argument to functions

%r10-r11

temporary

%r12-r15

callee-saved registers

标黄的三个寄存器是后面要用到;
另外还有一个重要的寄存器 %rip(instruction pointer register),指向下一条将要执行的指令。%rip不能被程序直接访问。 在gdb中,%rip被叫做 pc.

Stack Frame


x86-64的memory layout如下图, 栈由高地址向低地址增长。

每一个procedure call都将会导致一个stack frame的创建, 栈帧的组成如下图,可以看到栈帧范围内的内存寻址是通过寄存器 %rbp的偏移位置来完成,也就是说在一个栈帧中%rbp的值是固定的,所以可以拿%rbp来做基准参考,通过相对与%rbp的偏移在栈上进行寻址。 %rsp也拥有%rbp类似的寻址功能。

哪些指令可以修改%rsp寄存器呐?
PUSH,POP,CALL,RET,IRET,INT instruction implicitly use the stack pointer.

从上图可以看到每个栈帧中,首先被push了 “return address” 返回地址,也就是子程序ret后将要执行的原父函数的指令地址; 接着是保存了上一栈帧的%rbp值; 剩下的就是子函数的本地/临时变量。
关于函数入参,看到argument7 到 argument n是保存在 previous 栈帧上,它们的入栈顺序是从右到左。  前六个argument是保存在寄存器中,从register 表中可以找到相关的信息。

call & ret

与函数调用最密切的两个指令: call & ret。

一个问题是栈帧中的“return address”是 如何&何时 push到栈上的?

是call指令。执行call指令是,它做了两件事:
1.push %rip值到寄存器,前面说了%rip指向下一条执行指令,对于子函数来说就是返回地址了;
2.然后跳转到target地址。

ret 正好与call指令对应, pop 当前栈帧的“return address” 到 %rip寄存器,开始执行%rip指向的指令。

Toy example

来看一个栗子????吧,

#include

#include

 

int  trampoline_test(void);

 

int fun_a(int i, char *pchar)

{

 

        if (i == 5)

                goto next;

 

        printf("pchar is:%s\n", pchar);

        return 0;

 

next:

        printf("j is %d\n", i);

 

        return 1;

}

 

int main()

{

        char test[] = "Hellow";

        int i = 0;

 

        i = trampoline_test();

        i = 200;

        i += 8;

 

        printf("i==%d\n",i);

        fun_a(1, (char *)&test);

 

        return 0;

}

0000000100003e60 <_fun_a>:     //  entry fun_a之前,返回地址已经pushstack

100003e60: 55                           pushq   %rbp                  // 保存前一个栈帧的%rbp值;

100003e61: 48 89 e5                     movq    %rsp, %rbp    // 为当前栈帧 更新%rbp

100003e64: 48 83 ec 10                  subq    $16, %rsp      // 为本地变量预留栈空间

100003e68: 89 7d f8                     movl    %edi, -8(%rbp)

100003e6b: 48 89 75 f0                  movq    %rsi, -16(%rbp)

100003e6f: 83 7d f8 05                  cmpl    $5, -8(%rbp)

100003e73: 0f 85 05 00 00 00            jne     0x100003e7e <_fun_a+0x1e>

100003e79: e9 1e 00 00 00               jmp     0x100003e9c <_fun_a+0x3c>

100003e7e: 48 8b 75 f0                  movq    -16(%rbp), %rsi

100003e82: 48 8d 3d f5 00 00 00         leaq    245(%rip), %rdi  # 100003f7e

100003e89: b0 00                        movb    $0, %al

100003e8b: e8 cc 00 00 00               callq   0x100003f5c

100003e90: c7 45 fc 00 00 00 00         movl    $0, -4(%rbp)

100003e97: e9 18 00 00 00               jmp     0x100003eb4 <_fun_a+0x54>

100003e9c: 8b 75 f8                     movl    -8(%rbp), %esi

100003e9f: 48 8d 3d e5 00 00 00         leaq    229(%rip), %rdi  # 100003f8b

100003ea6: b0 00                        movb    $0, %al

100003ea8: e8 af 00 00 00               callq   0x100003f5c

100003ead: c7 45 fc 01 00 00 00         movl    $1, -4(%rbp)

100003eb4: 8b 45 fc                     movl    -4(%rbp), %eax

100003eb7: 48 83 c4 10                  addq    $16, %rsp      // 回收栈空间

100003ebb: 5d                           popq    %rbp                   // 恢复previous 栈帧 %rbp

100003ebc: c3                           retq                         // “return address”出栈,更新%rip

100003ebd: 0f 1f 00                     nopl    (%rax)


理解了函数如何返回到previous后,我们来定义一个函数trampoline_test(),修改该函数栈帧上返回地址的内容,使得从trampoline_test()返回后,跳过“i = 200”这条语句。
代码如下:
cat stack_frame.S
#include
#include
#include

.text

.global _trampoline_test
_trampoline_test:
pushq   %rbp
movq    %rsp, %rbp
subq    $8, %rsp
addq    $8, %rsp
popq    %rbp    // pop %rbp后,此时 %rsp指向了“return address
addq    $13, (%rsp)    // 等价于: %rsp  += 13,跳过13个字节的指令
movl    $2, %eax    // 返回值为2
retq


跳过的13个字节就是 下面高亮部分;

100003ef3: e8 4c 00 00 00               callq   0x100003f44 <_trampoline_test>

100003ef8: 89 45 f0                     movl    %eax, -16(%rbp)

100003efb: c7 45 f0 c8 00 00 00         movl    $200, -16(%rbp)

100003f02: 8b 45 f0                     movl    -16(%rbp), %eax

100003f05: 83 c0 08                     addl    $8, %eax    // trampoline_test返回值为2,2+8=10

100003f08: 89 45 f0                     movl    %eax, -16(%rbp)

100003f0b: 8b 75 f0                     movl    -16(%rbp), %esi

100003f0e: 48 8d 3d 86 00 00 00         leaq    134(%rip), %rdi  # 100003f9b

100003f15: b0 00                        movb    $0, %al

100003f17: e8 40 00 00 00               callq   0x100003f5c

100003f1c: 48 8d 7d f5                  leaq    -11(%rbp), %rdi


代码实际执行后的结果是:

$ as -o stack_frame.o stack_frame.S

$ gcc -c -o jump_test.o  jump_test.c

$ gcc jump_test.o  stack_frame.o -o jump_test

$./jump_test
i==10
pchar is:Hellow






上一篇:Arm64 FP/LR寄存器解析
下一篇:Linux special sections: exception_table