6

我想了解 C 调用约定。为此,我编写了以下代码:

#include <stdio.h>
#include <stdlib.h>

struct tstStruct
{
    void *sp;
    int k; 
};

void my_func(struct tstStruct*);

typedef struct tstStruct strc;

int main()
{
    char a;
    a = 'b';
    strc* t1 = (strc*) malloc(sizeof(strc));
    t1 -> sp = &a;
    t1 -> k = 40; 
    my_func(t1);
    return 0;   
}

void my_func(strc* s1)
{
        void* n = s1 -> sp + 121;
        int d = s1 -> k + 323;
}

然后我使用 GCC 和以下命令:

gcc -S test3.c

并提出了它的组装。我不会显示我得到的整个代码,而是粘贴函数 my_func 的代码。它是这个:

my_func:
.LFB1:
.cfi_startproc
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp
.cfi_def_cfa_register 6
movq    %rdi, -24(%rbp)
movq    -24(%rbp), %rax
movq    (%rax), %rax
addq    $121, %rax
movq    %rax, -16(%rbp)
movq    -24(%rbp), %rax
movl    8(%rax), %eax
addl    $323, %eax
movl    %eax, -4(%rbp)
popq    %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc

据我了解,情况是这样的:首先将调用者的基指针压入堆栈,并将其堆栈指针设为新的基指针,以便为新函数设置堆栈。但是其他的我不明白。据我所知,参数(或指向参数的指针)存储在堆栈中。如果是这样,第二条指令的目的是什么,

movq        -24(%rbp), %rax

这里,%rax 寄存器的内容被移动到距离寄存器 %rbp 中地址 24 个字节的地址。但是 %rax 中有什么????最初什么都没有存储在那里???我想我很困惑。请帮助了解此功能的工作原理。提前致谢!

4

3 回答 3

10

您将 AT&T 语法与 Intel 语法混淆了。

movq -24(%rbp), %rax

在英特尔语法中,它将是

mov rax,[rbp-24]

所以它将数据移动rbprax,反之亦然。操作数的顺序在 AT&T 语法中是 src、dest,而在 Intel 语法中是 dest、src。

然后,为了摆脱 GAS 指令以使反汇编更易于阅读,我使用 gcc 简单地组装代码gcc test3.c并使用ndisasm -b 64 a.out. 请注意,my_func下面由 NDISASM 生成的函数的反汇编采用 Intel 语法:

000005EF 55 推送 rbp
000005F0 4889E5 移动 rbp,rsp ; 创建堆栈帧。
000005F3 48897DE8 mov [rbp-0x18],rdi ; s1 到一个局部变量中。
000005F7 488B45E8 mov rax,[rbp-0x18] ; rax = s1(它是一个指针)
000005FB 488B00 移动拉克斯,[拉克斯];取消引用 rax,存储到 rax。
000005FE 4883C079 添加 rax,byte +0x79 ; 拉克斯 = 拉克斯 + 121
00000602 488945F8 mov [rbp-0x8],rax ; 无效* n = s1 -> sp + 121
00000606 488B45E8 移动拉克斯,[rbp-0x18];rax = 指向 s1 的指针
0000060A 8B4008 移动 eax,[rax+0x8] ; 取消引用 rax+8,存储到 eax。
0000060D 0543010000 添加 eax,0x143 ; eax = eax + 323
00000612 8945F4 移动 [rbp-0xc],eax ; 诠释 d = s1 -> k + 323
00000615 5D pop rbp
00000616 C3 RET

有关 Linux x86-64 调用约定 (System V ABI) 的信息,请参阅What are the calling conventions for UNIX & Linux system calls on x86-64 的答案。

于 2013-04-18T17:01:09.197 回答
7

该函数是这样分解的(我忽略了不必要的行):

首先,保存前一个堆栈帧:

pushq   %rbp
movq    %rsp, %rbp

在这里,旧%rbp的被压入堆栈以存储直到函数结束。然后,将%rbp其设置为新的值%rsp(它是保存%rbppush发生的下一行)。

movq    %rdi, -24(%rbp)

在这里你首先要知道i386 system V ABIamd64 system V ABI之间的主要区别之一。

在 i386 System V ABI 中,函数参数通过堆栈(并且仅通过堆栈)传递。相反,在 amd64 System V ABI 中,参数首先通过寄存器传递(如果是整数,%rdi如果是浮点数%rsi,则为to )。一旦寄存器的数量用完,剩下的参数就会像 i386 一样被压入堆栈。%rdx%rcx%r8%r9%xmm0%xmm7

所以,在这里,机器只是将函数的第一个参数(它是一个整数)临时加载到堆栈上。

movq    -24(%rbp), %rax

因为您不能直接将数据从一个寄存器传输到另一个寄存器,所以 的内容%rdi随后被加载到%rax. 所以,%rax现在存储这个函数的第一个(也是唯一的)参数。

movq    (%rax), %rax

该指令只是取消引用指针并将结果存储回%rax.

addq    $121, %rax

我们将 121 添加到指定的值。

movq    %rax, -16(%rbp)

我们将获得的值存储到堆栈中。

movq    -24(%rbp), %rax

我们再次加载函数的第一个参数%rax(请记住,我们将第一个参数存储在-24(%rbp))。

movl    8(%rax), %eax
addl    $323, %eax

如前所述,我们取消引用指针并将获得的值存储在其中%eax,然后将 323 添加到它并放回%eax.

请注意,这里我们从 切换%rax到 是%eax因为我们正在处理的值不再void*像以前那样是(64 位),而是int(32 位)。

movl    %eax, -4(%rbp)

最后,我们将这个计算的结果存储到堆栈中(这在这里似乎没有用,但编译器在编译时没有检测到它可能是不必要的)。

popq    %rbp
ret

最后两条指令只是在将手交还给main函数之前恢复先前的堆栈帧。

我希望这能让这种行为现在更清楚。

于 2013-04-18T18:34:19.447 回答
1

您可以通过输入以下命令更改为 intel 语法:

$ gcc -S -masm=intel test3.c -o test3.s
于 2015-10-26T21:07:19.383 回答