delphi中的函数传参如何传枚举参数_函数调用背后的秘密——传参与返回值

7e12b9affa14076e38fbbdad42c55467.png

函数调用是解耦思想的一个基本体现。为了实现调用,我们往往需要给子程序提供一些参数并得到子程序的执行结果。今天就来探究一下这个过程在计算机内部是如何实现的。更多内容欢迎来我的博客转转:

精神的壳​qiuyueqy.com
b4b78eb34271767ce582492e54ccfa9e.png

利用寄存器传参

来看例一:

int square(int num) {
    return num * num;
}

int main() {
    int k = 2;
    square(k);
}

在 x86-64 平台上汇编得到的结果是这样的:

square:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        imul    eax, eax
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 2
        mov     eax, DWORD PTR [rbp-4]
        mov     edi, eax
        call    square
        mov     DWORD PTR [rbp-4], eax
        mov     eax, 0
        leave
        ret

变量 k 保存在内存 [rbp-4] 处,其值为 2。随后 [rbp-4] 中的值被存到了 eax,最后保存在 edi,这就完成了一次寄存器传参。square 函数更新 rbp 的值再从 edi 取值保存到 [rbp-4],完成形参到实参的复制。square 把结果保存在 eax 里,main 读取 eax 得到调用返回值。

用栈传参

当参数的数量过多无法全部保存到寄存器里或长度超过了机器字长,就要用到栈来传参。再来看例二:

typedef struct {
    char name[20];
    int  age;
}Person;

void greet(Person people) {
    printf("hello %s, you are %d years old.n", people.name, people.age);
}

int main() {
    Person me = {name: "Rowan", age: 21};
    greet(me);
}

翻译成汇编语言是这样:

.LC0:
        .string "hello %s, you are %d years old.n"
greet:
        push    rbp
        mov     rbp, rsp
        mov     eax, DWORD PTR [rbp+36]
        mov     edx, eax
        lea     rsi, [rbp+16]
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        nop
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
        movabs  rax, 474081619794
        mov     edx, 0
        mov     QWORD PTR [rbp-32], rax
        mov     QWORD PTR [rbp-24], rdx
        mov     DWORD PTR [rbp-16], 0
        mov     DWORD PTR [rbp-12], 21
        sub     rsp, 8
        push    QWORD PTR [rbp-16]
        push    QWORD PTR [rbp-24]
        push    QWORD PTR [rbp-32]
        call    greet
        add     rsp, 32
        mov     eax, 0
        leave
        ret

一个 Person 实例占 24 字节,超过了 x86-64 平台下一个通用寄存器的最大长度(8 字节),编译器用栈来保存参数。

26 到 28 行的三个 push 操作把 me 入栈,随后用 call 指令跳到 square 函数。square 函数在栈的 [rbp+36] 取到 age 成员的值,取 [rbp+16] (name 成员)的内存地址放入 rsi,完成一次传参。

细探 sp 与 bp

sp(stack pointer),中文名“栈顶寄存器”。顾名思义,这个指针时刻指向栈的顶部,其值随 push 和 pop 操作隐式改变。假设操作对象为 x:

  • push x:首先,sp -= len(x)(x 的字长,以字节为单位);然后,在 [sp-len(x), sp) 中存入 x 值。
  • pop x:首先,从 [sp-len(x), sp) 中读取数据存放到 x;然后,sp += len(x)。

如果编译器能推断一个函数要占用多少空间,在进入这个函数时往往会为它预先分配好一段栈空间(如例二汇编代码第 18 行 sub rsp, 32,为 main 函数分配了 32 字节栈空间)。在这个函数退出时,调整 sp 的值销毁这段空间(例二汇编代码第 30 行 add rsp, 32)。

bp(base pointer),中文名“基指寄存器”。因为随着指令执行 sp 是不断变化的,所以在进入函数时常用 bp 保存 sp 的初始值以对要用到的数据进行寻址定位(例二汇编代码第 6 行 mov eax, DWORD PTR [rbp+36],第 21 行 mov QWORD PTR [rbp-32], rax 等)。

指令 call x 会设置 ip 寄存器的值为 x 的段偏移地址,达到“调用函数”的目的。这个过程分为两步:

  1. 把 ip 寄存器的原址入栈(此时 ip 指向 call 的下一条指令),相当于 push ip
  2. 更新 ip 寄存器的值,相当于 mov ip, x

指令 ret 从栈顶取值保存到 ip 寄存器,相当于 call 的逆操作。执行完 ret 指令后 ip 恢复到函数调用前的值,程序从而继续向下执行。

指令 leave 在函数执行结束后恢复 sp 和 bp 的值。上文说到进入函数时会把 sp 保存到 bp 再调整 sp 的值以为函数分配栈空间。那么在退出函数时自然要恢复原来的 sp 和 bp。这条指令相当于 mov rsp, rbppop rbp 的结合。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值