分类: 汇编

  • x86-64系统架构:栈和栈帧

    本文汇编语言均使用AT&T风格。

    1. 栈

    栈是一种线性数据结构,它遵循后进先出(LIFO,Last In First Out)原则。

    栈是处理器执行汇编代码时使用的特殊内存区域,是函数调用的重要组成部分。

    栈是内存中的一段连续空间,由RSP/RBP寄存器标记,硬件指令PUSH/POP直接操作栈,增长方向从内存高地址到低地址。栈内存由操作系统为程序分配。

    RBP(Base Pointer)栈帧基指针,指向栈帧的“底部”(实际上是栈帧中最高的内存地址)。用作访问函数参数和局部变量的参考点。

    RSP(Stack Pointer)栈指针,指向栈的“顶部”,这里是栈的最低的内存地址,随着栈的增长,RSP会被递减。

    通过以下代码示例说明栈和栈帧,C代码如下:

    int sum(int a1,int a2,int a3, int a4, int a5,int a6,int a7,int a8) {
        return a1+a2+a3+a4+a5+a6+a7+a8;
    }
    
    int sumcalc() {
        return sum(1,2,3,4,5,6,7,8);
    }

    通过gcc命令编译:“`gcc -g -o test test.c“`

    再通过GDB进行反汇编函数 sumcalc和sum:

    (gdb) disassemble/r sumcalc
    Dump of assembler code for function sumcalc:
       0x000000000000116d <+0>:     f3 0f 1e fa         endbr64
       0x0000000000001171 <+4>:     55                  push   %rbp
       0x0000000000001172 <+5>:     48 89 e5            mov    %rsp,%rbp
       0x0000000000001175 <+8>:     6a 08               push   $0x8
       0x0000000000001177 <+10>:    6a 07               push   $0x7
       0x0000000000001179 <+12>:    41 b9 06 00 00 00   mov    $0x6,%r9d
       0x000000000000117f <+18>:    41 b8 05 00 00 00   mov    $0x5,%r8d
       0x0000000000001185 <+24>:    b9 04 00 00 00      mov    $0x4,%ecx
       0x000000000000118a <+29>:    ba 03 00 00 00      mov    $0x3,%edx
       0x000000000000118f <+34>:    be 02 00 00 00      mov    $0x2,%esi
       0x0000000000001194 <+39>:    bf 01 00 00 00      mov    $0x1,%edi
       0x0000000000001199 <+44>:    e8 8b ff ff ff      call   0x1129 <sum>
       0x000000000000119e <+49>:    48 83 c4 10         add    $0x10,%rsp
       0x00000000000011a2 <+53>:    c9                  leave
       0x00000000000011a3 <+54>:    c3                  ret
    
    
    (gdb) disassemble/r sum
    Dump of assembler code for function sum:
       0x0000000000001129 <+0>:     f3 0f 1e fa   endbr64
       0x000000000000112d <+4>:     55            push   %rbp
       0x000000000000112e <+5>:     48 89 e5      mov    %rsp,%rbp
       0x0000000000001131 <+8>:     89 7d fc      mov    %edi,-0x4(%rbp)
       0x0000000000001134 <+11>:    89 75 f8      mov    %esi,-0x8(%rbp)
       0x0000000000001137 <+14>:    89 55 f4      mov    %edx,-0xc(%rbp)
       0x000000000000113a <+17>:    89 4d f0      mov    %ecx,-0x10(%rbp)
       0x000000000000113d <+20>:    44 89 45 ec   mov    %r8d,-0x14(%rbp)
       0x0000000000001141 <+24>:    44 89 4d e8   mov    %r9d,-0x18(%rbp)
       0x0000000000001145 <+28>:    8b 55 fc      mov    -0x4(%rbp),%edx
       0x0000000000001148 <+31>:    8b 45 f8      mov    -0x8(%rbp),%eax
       0x000000000000114b <+34>:    01 c2         add    %eax,%edx
       0x000000000000114d <+36>:    8b 45 f4      mov    -0xc(%rbp),%eax
       0x0000000000001150 <+39>:    01 c2         add    %eax,%edx
       0x0000000000001152 <+41>:    8b 45 f0      mov    -0x10(%rbp),%eax
       0x0000000000001155 <+44>:    01 c2         add    %eax,%edx
       0x0000000000001157 <+46>:    8b 45 ec      mov    -0x14(%rbp),%eax
       0x000000000000115a <+49>:    01 c2         add    %eax,%edx
       0x000000000000115c <+51>:    8b 45 e8      mov    -0x18(%rbp),%eax
       0x000000000000115f <+54>:    01 c2         add    %eax,%edx
       0x0000000000001161 <+56>:    8b 45 10      mov    0x10(%rbp),%eax
       0x0000000000001164 <+59>:    01 c2         add    %eax,%edx
       0x0000000000001166 <+61>:    8b 45 18      mov    0x18(%rbp),%eax
       0x0000000000001169 <+64>:    01 d0         add    %edx,%eax
       0x000000000000116b <+66>:    5d            pop    %rbp
       0x000000000000116c <+67>:    c3            ret

    通过上述反汇编代码可以得出如图所示的栈&帧结构如下:

    2. 栈帧

    当调用函数时会建立一个栈帧以支持该函数。栈帧由函数序言(prologue)建立,通过函数后记(epilogue)进行清理。

    函数序言和后记均由编译器生成,固定模板如下:

    函数序言&后记:

    sum:
        # 函数序言
        push %rbp        # 保存caller函数的rbp
        mov %rsp, %rbp   # 生成当前函数callee的帧基址,callee函数帧的起始位置
        
        
        # 函数后记
        mov %rbp, %rsp   # 释放当前函数callee的栈帧
        pop %rbp         # 恢复caller的栈帧
        ret              # 从栈中弹出返回地址并跳到该地址,控制权返回给caller

    如上图所示,栈帧中通常包含以下组成部分:

    1)返回地址

    2)保存的基指针(RBP)

    3)局部变量

    4)函数参数(当函数参数的数量大于6个时使用栈传递)

  • x86-64汇编之Hello world

    1. 环境

    WSL子系统:Ubuntu

    2. 工具

    所需工具包列表:

    程序名称检查命令所需的包
    GUN Assembleras –versionbinutils
    GUN Linkerld –versionbinutils
    GUN Debuggergdb –versiongdb
    Makemake –versionmake

    3. 代码

    代码使用语法风格是:AT&T。

    .global _start
    .section .data
    mystr:
        .string "Hello, world!\n"
    mystrend:
    .section .text
    _start:
        # 设置系统调用号,1 表示write
        movq $1, %rax
        # 标准输出stdout
        movq $1, %rdi
        # 字符串指针
        leaq mystr(%rip), %rsi
        # 字符串长度
        movq $(mystrend-mystr), %rdx
        # 执行系统调用
        syscall
    
        # 设置系统调用号,60 表示exit
        movq $60, %rax
        syscall
    

    4.编译链接并执行

    # 编译
    as helloworld.s -o helloworld.o
    # 链接
    ld helloworld.o -o helloworld
    # 执行,控制台会输出 :Hello, world!
    ./helloworld

    参考资料:

    1. x64架构体系的系统调用列表:https://x64.syscall.sh/