C函数的汇编代码

5
我正在尝试理解C函数的汇编代码。我不理解为什么在main函数中执行了"andl -16"操作。这是为了为局部变量分配空间吗?如果是这样,那么为什么在main函数中还要执行"subl 32"操作?
我无法理解"func1"的反汇编代码。对于8086处理器,堆栈从高地址往低地址增长。那么为什么ebp(用于参数偏移)的访问是在正向而非负向?在func1函数内部的局部变量为3 + 返回地址 + 已保存寄存器,总共应该是20字节,但为什么需要执行"subl $24,esp"操作呢?
#include<stdio.h>
int add(int a, int b){
 int res = 0;
 res = a + b;
 return res;
}
int func1(int a){
 int s1,s2,s3;
 s1 = add(a,a);
 s2 = add(s1,a);
 s3 = add(s1,s2);
 return s3;
}
int main(){
 int a,b;
 a = 1;b = 2;
 b = func1(a);
 printf("\n a : %d b : %d \n",a,b);
 return 0;
}

汇编代码:

       .file   "sample.c"
        .text
.globl add
        .type   add, @function
add:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $16, %esp
        movl    $0, -4(%ebp)
        movl    12(%ebp), %eax
        movl    8(%ebp), %edx
        leal    (%edx,%eax), %eax
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        leave
        ret
        .size   add, .-add
.globl func1
        .type   func1, @function
func1:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    8(%ebp), %eax
        movl    %eax, 4(%esp)
        movl    8(%ebp), %eax
        movl    %eax, (%esp)
        call    add
        movl    %eax, -4(%ebp)
        movl    8(%ebp), %eax
        movl    %eax, 4(%esp)
        movl    -4(%ebp), %eax
        movl    %eax, (%esp)
        call    add
        movl    %eax, -8(%ebp)
        movl    -8(%ebp), %eax
        movl    %eax, 4(%esp)
        movl    -4(%ebp), %eax
        movl    %eax, (%esp)
                                      call    add
        movl    %eax, -12(%ebp)
        movl    -12(%ebp), %eax
        leave
        ret
        .size   func1, .-func1
        .section        .rodata
.LC0:
        .string "\n a : %d b : %d \n"
        .text
.globl main
        .type   main, @function
main:
        pushl   %ebp
        movl    %esp, %ebp
        andl    $-16, %esp
        subl    $32, %esp
        movl    $1, 28(%esp)
        movl    $2, 24(%esp)
        movl    28(%esp), %eax
        movl    %eax, (%esp)
        call    func1
        movl    %eax, 24(%esp)
        movl    $.LC0, %eax
        movl    24(%esp), %edx
        movl    %edx, 8(%esp)
        movl    28(%esp), %edx
        movl    %edx, 4(%esp)
        movl    %eax, (%esp)
        call    printf
        movl    $0, %eax
        leave
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.4.4-14ubuntu5) 4.4.5"
        .section        .note.GNU-stack,"",@progbits
2个回答

5
andl $-16, %esp将堆栈指针对齐到16字节的倍数,通过清除低四位实现。
只有使用(%ebp)正偏移访问参数时才会使用正偏移量。
你未说明目标平台是什么,或者你用来编译的开关是什么。汇编代码显示插入了某个Ubuntu标识符,但我不熟悉它使用的ABI,除了它可能类似于Intel x86架构通常使用的ABI。因此,我猜测ABI在例程调用时需要8字节对齐,因此编译器使func1的堆栈帧为24字节而不是20字节,以保持8字节对齐。
我进一步猜测编译器在main开始时将堆栈对齐到16字节作为编译器的“首选项”,以防它使用偏爱16字节对齐的SSE指令或其他操作。
所以,我们有:
main中,andl $-16, %esp将堆栈对齐到16字节的倍数作为编译器的首选项。在main内部,28(%esp)24(%esp)引用编译器保存在堆栈上的临时值,而8(%esp)4(%esp)(%esp)用于将参数传递给func1printf。从汇编代码调用printf但被注释掉的事实以及您粘贴的C源代码与生成汇编代码使用的C源代码不同可以得知:这不是从C源代码生成的正确汇编代码。
func1中,分配了24字节的堆栈而不是20字节以保持8字节对齐。在func1内部,通过8(%ebp)4(%ebp)访问参数。位置-12(%ebp)-4(%ebp)用于保存变量的值。4(%esp)(%esp)用于将参数传递给add
下面是func1的堆栈帧:
    - 4(%ebp) = 20(%esp): s1。
    - 8(%ebp) = 16(%esp): s2。
    -12(%ebp) = 12(%esp): s3。
    -16(%ebp) =  8(%esp): 未使用的填充。
    -20(%ebp) =  4(%esp): 传递add的第二个参数。
    -24(%ebp) =  0(%esp): 传递add的第一个参数。

2
这篇来自gcc手册的引用可能会有所帮助:-mpreferred-stack-boundary=num: 试图将堆栈边界保持对齐到2的num次方字节边界。如果未指定-mpreferred-stack-boundary,则默认值为4(16字节或128位)。 - teppic
感谢您解释8字节对齐。我的疑问在于add->1(局部变量)+返回地址=>2 * 8 = 16,这是正确的。但在func1中->3(局部变量)+返回地址=>4 * 8 = 32应该存在。为什么是24?为什么在主函数中要将32减去基指针?我对所做的计算有点困惑。 - Angus
1
@Angus:在计算func1大小时,为什么要乘以8?在这个32位架构中,func1和地址中的int对象每个都是四个字节。func1使用了四个字节来存储s1,四个字节来存储s2,四个字节来存储s3,八个字节用于传递两个参数给add函数,再加上四个字节进行对齐填充。总共是24个字节。 - Eric Postpischil
谢谢Eric。我现在明白了。 - Angus
@eric:在主函数中,我仍然不明白为什么要执行 andl -16, ebp,然后再执行 subl $32, ebp。我们通过执行 andl -16, ebp 为主函数中的局部变量分配了 16 字节的空间。那么为什么还要执行 subl 32, ebp? - Angus
1
@Angus:%esp 的低四位具有从零到十五的某些值。 andl $-16,%esp 清除这些位,将%esp 减少到16的倍数。 如果%esp 已经是16的倍数,则不会更改。 因为您事先不知道%esp 将减少多少,所以不能指望其中任何字节用于堆栈帧。 它们仅用于填充并保持未使用状态。 因此,您必须从%esp 中减去32以获得所需的字节。 - Eric Postpischil

3

我建议使用objdump -S的输出进行工作,这将为您提供与C源代码的交叉列表。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接