ILD

i386汇编实战:函数
作者:Herbert Yuan 邮箱:yuanjp89@163.com
发布时间:2017-7-3 站点:Inside Linux Development

1 调用约定

调用约定规定了哪些寄存器由调用者保存、哪些寄存器由被调用者恢复、参数如何传递、栈由谁清除等。有很多调用约定如cdecl、fastcall、stdcall等。通常调用约定是Application Binary Interface (ABI) 标准的一部分。

在Linux世界里,gcc是事实上的标准。gcc约定:

调用者:caller-saved register包括%EAX, %ECX, %EDX,他们被首先压入栈中。参数以按调用顺序相反的顺序压入栈中(第一个参数最后入栈),参数是以32位存储的。64位参数用两个32位存储,其它参数一律占32位。返回地址入栈。

被调用者:EBP入栈,ESP->EBP,局部变量分配栈,如果有临时数据,也分配栈,如果使用了callee saved registers (EBX, EDI, ESI),把它们也压入栈。

32位及以下整数返回值存储到EAX寄存器中。

栈是从上向下生长的,栈的分布如下:

图1 stack内容排列

栈中的上述内容,只有返回地址和调用者的ebp是必须的,其它是可选的,例如,如果调用者没有用到%edx,则调用者无需将其压入栈,不管被调用者将其改成啥,我都不关心。

gcc约定栈对齐到16字节。

2 最简单函数

添加汇编文件func.s,其包含一个全局变量var和一个函数func。func设置全局变量var的值为1。直接贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    .globl  var
    .data
    .align 4
    .type   var, @object
    .size   var, 4
var:
    .long   10
    .text
    .global func
    .type       func, @function
func:
    pushl %ebp
    movl %esp, %ebp
    movl $0x1, var
    leave
    ret

func函数汇编指令解释:

  1. 前两行,保存父函数的栈。

  2. movl $0x1, var,将立即数1移动到var。

  3. leave,恢复父函数的栈。

  4. ret,返回到父函数执行。

添加调试main.c代码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
extern int var;
extern void func();
 
int main(int argc, char **argv)
{
    func();
    printf("var changed to %d\n", var);
    return 0;
}

编译: cc -m32 func.s main.c。运行./a.out结果:

1
2
herbert@Lenovo:/work/assembly/i386/func$ ./a.out
var changed to 1

由于我的linux系统是64位,所以使用-m32选项,编译32位可执行文件。

3 参数传递

参数可以通过ebp找到,参数n(0是第一个参数)的地址为:ebp + 8 + 4 * n;

下面将传入的参数写入到var。

1
2
3
4
5
6
7
8
func:
    pushl %ebp
    movl %esp, %ebp
    mov 8(%ebp), %eax
    movl %eax, var
    nop
    popl %ebp
    ret

参考gcc的编译结果, 这里有一点改进。leave替换成了nop和popl %ebp。因为没有局部变量,ebp和esp相等。所以没有必要将ebp拷贝到esp。

修改main.c

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
extern int var;
extern void func(int n);
 
int main(int argc, char **argv)
{
    func(255);
    printf("var changed to %d\n", var);
    return 0;
}

编译后执行:

1
2
herbert@Lenovo:/work/assembly/i386/func$ ./a.out
var changed to 255

4 返回结果

返回结果,只将结果存储到EAX寄存器即可。

下面,将var作为返回结果。

1
2
3
4
5
6
func:
    pushl %ebp
    movl %esp, %ebp
    movl var, %eax
    leave
    ret

修改main.c

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
extern int var;
extern int func();
 
int main(int argc, char **argv)
{
    int a = func();
    printf("a is %d\n", a);
    return 0;
}

编译后执行结果

1
2
herbert@Lenovo:/work/assembly/i386/func$ ./a.out
a is 10

5 局部变量

局部变量,在紧跟着ebp指针指向的地址下面。cc编译结果显示,先定义的变量后入栈。使用局部变量时,需要维护栈,使其向下增长,且需要对齐到16字节。

下面的定义两个局部变量,返回它们的和。

1
2
3
4
5
6
7
8
9
10
func:
    pushl %ebp
    movl %esp, %ebp
    subl $16, %esp
    movl $1, -4(%ebp)
    movl $2, -8(%ebp)
    movl -4(%ebp), %eax
    addl -8(%ebp), %eax
    leave
    ret

main.c不变,运行结果如下:

1
2
herbert@Lenovo:/work/assembly/i386/func$ ./a.out
a is 3

6 使用 callee-saved registers

使用push和pop相关寄存器即可,如果没有用到这些寄存器,可不需要压栈。

示例:略

参考

https://www.csee.umbc.edu/~chang/cs313.s02/stack.shtml

Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.