zoukankan      html  css  js  c++  java
  • 函数调用过程与栈帧结构

    进程内存布局

    32 位保护模式下 Linux 中进程的内存布局如下:

    
        0xFFFFFFFF  ------> +----------------------+ <--+ 
                            |                      |    | 
                            |       OS Kernel      |    | 1GB
                            |                      |    |
        0xC0000000  ------> +----------------------+ <--+ 
                        |   |                      |    |
                        |   |         stack        |    |
                        v   |                      |    |
                            +----------------------+    |
                            |                      |    |
                            +----------------------+    |
                            | Memory Mapping Region|    |
        0x40000000  ------> +----------------------+    |
                            |                      |    |
                            +----------------------+    |
                        ^   |                      |    |
                        |   |         heap         |    | 3GB
                        |   |                      |    |
                            +----------------------+    |
                            |  Read/Write Segments |    |
                            |        .bss          |    |
                            |        .data         |    |
                            +----------------------+    |
                            |  Read-only Segments  |    |
                            | .init .text .rodata  |    |
                            |                      |    |
        0x08048000  ------> +----------------------+    |
                            |       reserved       |    |
        0x00000000  ------> +----------------------+ <--+
    
    

    32 位下寻址空间为 4GB,其中高地址的 1GB 是给操作系统内核使用的(Windows 中默认为 2GB),称为内核空间(Kernel Space),剩余的 3GB 分配给应用程序使用,称为用户空间(User Space)。当进程运行在内核空间时就处于内核态(Kernel Mode),当进程运行在用户空间时就处于用户态(User Mode)。

    区分用户态和内核态是为了提高系统的稳定性和安全性,使操作系统能够控制资源的访问,能够防止应用程序执行一些危险的指令。计算机体系结构中,在硬件上提供不同的特权态,即 Rings Protection,如 Intel CPU 有 4 个特权级:Ring0、Ring1、Ring2、Ring3,一般操作系统只使用 Ring0 和 Ring3,Ring0 具有最高权限,能够访问任何资源,Ring3 访问受限,需要陷入(trap)到内核态才能访问特权资源。

    (stack)从虚拟地址 0xC0000000 往低地址增长。(heap)正好相反,从低地址往高地址增长。栈用于函数调用以及存放局部变量等,堆用于动态内存分配,如 C 语言中的 malloc() 函数。栈与操作系统内核之间有一个随机的 offset,堆与读/写段之间也有一个随机的 offset。

    内存映射段(Memory Mapping Segment)用于将文件内容映射到内存,用于加载动态链接库,可以通过 mmap() 系统调用实现内存映射。

    bss 段放的是未初始化的全局变量和静态变量,默认都初始化为 0,data 段存放的是初始化的全局变量和静态变量。

    text 段存放的是程序代码,除了可读还具有可执行权限。

    栈及其操作

    栈是一种先进后出的结构,包含两种操作:pushpop

    在 IA-32 体系结构中,通常用 ESPEBP 维护一个栈,EBP 指向栈底,ESP 指向栈顶。

    IA-32 中 PUSH 指令先减少 ESP 的值,再将源操作数复制到堆中。

    PUSH EAX 等价于

    SUB ESP, 4
    MOV [ESP], EAX
    

    POP 指令正好相反,如 POP EBX 等价于

    MOV EBX, [ESP]
    ADD ESP, 4
    

    栈帧

    栈帧(stack frame)用于函数调用,每一次函数调用都会有一个独立的栈帧,包含了返回地址、局部变量、上下文等信息。

                            
                high address        
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                   +------> |    previous EBP    |    |
                   |        +--------------------+    |
                   |        |  saved registers   |    |
                   |        +--------------------+    |
                   |        |  local variables   |    | caller's frame
                   |        +--------------------+    |
                   |        |   callee's args    |    |
                   |        +--------------------+    |
                   |        |   return address   |    |
                   |        +--------------------+ <--+
                   +------- |    previous EBP    |    |
                            +--------------------+    | 
                            |  local variables   |    |
                            +--------------------+    |
                            |                    |    | callee's frame
                            |                    |    |
                            |                    |    |
                            |                    |    |
                            |                    |    |
                            +--------------------+ <--+
                low address      
    
    
    

    调用者(Caller) 调用 被调用者,每次调用都会有新的栈帧压栈,所以一般深度优先搜索可以用栈来代替递归,以达到更深的搜索深度。

    ESPEBP 只维护当前的栈帧,因此之前的栈帧都要保存下来,栈帧的顶部保存了上一个栈帧的 EBP 指向的位置,函数返回时 EBP 能够恢复到上一个栈帧的 EBP

    函数调用过程

    调用过程

    调用者 调用 被调用者 前,先保存返回地址,即下一条指令的地址,用于返回后继续执行,然后进入被调用者函数。

    被调用者开辟了新的栈帧,因此需要保存调用者的栈帧,通常使用以下两条指令:

    PUSH EBP
    MOV EBP, ESP
    

    先把旧的 EBP 入栈,然后让 EBP 指向旧的 EBP,此时 EBP 已经作为新的栈帧的栈底了。

    函数调用时,为了防止寄存器被覆盖,有时需要将寄存器内容也保存到栈中。

    参数的传递

    IA-32 下,在调用者函数中,参数从右往左入栈。进入被调用者函数时,参数以及局部变量的访问以 EBP 为基址,通过 EBP 加上偏移量访问。

    x86-64 下参数传递略有不同。如果函数调用参数少于 7 个,则用寄存器传递参数,参数从左到右依次放入寄存器 RDIRSIRDXRCXR8R9。参数超过 7 个时,前 6 个参数同样通过寄存器传参,之后的参数与 32 位一样从右往左压入栈中。

    返回过程

    返回值保存在 EAX 寄存器中。

    返回时先将 EBP 恢复至上一个栈帧的栈底,通常使用以下两条指令:

    MOV ESP, EBP
    POP EBP
    

    然后将保存的返回地址放入 EIP 寄存器。

    最后将保存的参数出栈。

    涉及的指令

    • CALL 指令

    先将当前 EIP(即下一条指令的地址作为返回地址)压入栈中,然后 EIP 转移到被调用者的入口地址。

    • RET 指令

    从栈顶弹出原来保存的地址至 EIP。

    • LEAVE 指令

    通常将返回时涉及的两条指令用 LEAVE 指令代替,也就是说 LEAVE 等价于上述提到的两条指令:

    MOV ESP, EBP
    POP EBP
    

    函数的调用约定

    上述参数从右往左入栈、返回值存入 EAX 寄存器等不是硬性规定的,而是遵守函数的调用约定(calling convention)。函数的调用约定规定了参数的传递顺序、参数和返回值放置的位置、调用前后设置的工作由调用者完成还是被调用者完成等。上文提到的都是 C 调用约定(cdecl调用约定)。其他的调用约定有 stdcall调用约定、fastcall调用约定、thiscall调用约定等。

    一个简单的例子

    以一个加法函数为例:

    // file: add.c
    #include <stdio.h>
    
    int add(int a, int b) {
        int c = a + b;
        return c;
    }
    
    int main() {
        int a = 1, b = 2;
        int c = add(a, b);
        printf("%d
    ", c);
        return 0;
    }
    

    编译成 32 位程序:

    gcc add.c -o add -m32
    

    查看汇编代码(汇编代码可以用 gdb、objdump、IDA 等工具查看,也可以直接用 gcc 编译成汇编代码)。

    输入 layout asm 查看汇编代码:

    进入 main() 函数后,先保存上一个栈帧的 EBP(main 函数不是第一个被调用的函数)。

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
            ESP,EBP ------> |    previous EBP    |    | main()'s frame
                            +--------------------+ <--+
                low address       
    
    

    然后保存寄存器。

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                EBP ------> |    previous EBP    |    |
                            +--------------------+    | main()'s frame
                ESP ------> |  saved registers   |    |
                            +--------------------+ <--+
                low address       
    
    

    开辟一定的空间保存局部变量。

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                EBP ------> |    previous EBP    |    |
                            +--------------------+    |
                            |  saved registers   |    |
                            +--------------------+    |
                            |       ......       |    |
                            +--------------------+    | main()'s frame
                            |        b=2         |    |
                            +--------------------+    |
                            |        a=1         |    |
                            +--------------------+    |
                ESP ------> |                    |    |
                            +--------------------+ <--+
                low address       
    
    

    压入 add() 函数的参数。

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                EBP ------> |    previous EBP    |    |
                            +--------------------+    |
                            |  saved registers   |    |
                            +--------------------+    |
                            |       ......       |    |
                            +--------------------+    |
                            |        b=2         |    | main()'s frame
                            +--------------------+    |
                            |        a=1         |    |
                            +--------------------+    |
                            |         2          |    |
                            +--------------------+    |
                ESP ------> |         1          |    |
                            +--------------------+ <--+
                low address       
    
    

    call add() 函数,先保存下一条指令的地址,然后跳转到 add() 函数。

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                EBP ------> |    previous EBP    |    |
                            +--------------------+    |
                            |  saved registers   |    |
                            +--------------------+    |
                            |       ......       |    |
                            +--------------------+    |
                            |        b=2         |    |
                            +--------------------+    | main()'s frame
                            |        a=1         |    |
                            +--------------------+    |
                            |         2          |    |
                            +--------------------+    |
                            |         1          |    |
                            +--------------------+    |
                ESP ------> |       0x122b       |    |
                            +--------------------+ <--+
                low address       
    
    

    进入 add() 函数,先保存 main() 函数的 EBP

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                            |    previous EBP    |    |
                            +--------------------+    |
                            |  saved registers   |    |
                            +--------------------+    |
                            |       ......       |    |
                            +--------------------+    |
                            |        b=2         |    |
                            +--------------------+    | main()'s frame
                            |        a=1         |    |
                            +--------------------+    |
                            |         2          |    |
                            +--------------------+    |
                            |         1          |    |
                            +--------------------+    |
                            |       0x122b       |    |
                            +--------------------+ <--+
            ESP,EBP ------> |    previous EBP    |    | add()'s frame
                            +--------------------+ <--+
                low address       
    
    

    开辟空间用以保存局部变量。

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                            |    previous EBP    |    |
                            +--------------------+    |
                            |   saved registers  |    |
                            +--------------------+    |
                            |       ......       |    |
                            +--------------------+    |
                            |        b=2         |    |
                            +--------------------+    | main()'s frame
                            |        a=1         |    |
                            +--------------------+    |
                            |         2          |    |
                            +--------------------+    |
                            |         1          |    |
                            +--------------------+    |
                            |       0x122b       |    |
                            +--------------------+ <--+
                EBP ------> |    previous EBP    |    |
                            +--------------------+    |
                            |       ......       |    | add()'s frame
                            +--------------------+    |
                ESP ------> |                    |    |
                            +--------------------+ <--+
                low address       
    
    

    通过 EBP 加上偏移获取 add() 函数的两个参数,然后求和,结果保存在 EAX 寄存器(EAX 是累加寄存器)。

    累加结果放入局部变量 c。

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                            |    previous EBP    |    |
                            +--------------------+    |
                            |   saved registers  |    |
                            +--------------------+    |
                            |       ......       |    |
                            +--------------------+    |
                            |        b=2         |    |
                            +--------------------+    | main()'s frame
                            |        a=1         |    |
                            +--------------------+    |
                            |         2          |    |
                            +--------------------+    |
                            |         1          |    |
                            +--------------------+    |
                            |       0x122b       |    |
                            +--------------------+ <--+
                EBP ------> |    previous EBP    |    |
                            +--------------------+    |
                            |        c=3         |    |
                            +--------------------+    | add()'s frame
                            |       ......       |    |
                            +--------------------+    |
                ESP ------> |                    |    |
                            +--------------------+ <--+
                low address      
    
    

    将 c 的值放入 EAX 寄存器作为返回值。

    然后 add() 函数返回,先执行 LEAVE 指令,恢复 main() 函数的 EBP

    MOV ESP, EBP
    
                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                            |    previous EBP    |    |
                            +--------------------+    |
                            |  saved registers   |    |
                            +--------------------+    |
                            |       ......       |    |
                            +--------------------+    |
                            |        b=2         |    |
                            +--------------------+    | main()'s frame
                            |        a=1         |    |
                            +--------------------+    |
                            |         2          |    |
                            +--------------------+    |
                            |         1          |    |
                            +--------------------+    |
                            |       0x122b       |    |
                            +--------------------+ <--+
            ESP,EBP ------> |    previous EBP    |    |
                            +--------------------+    |
                            |        c=3         |    |
                            +--------------------+    | add()'s frame
                            |       ......       |    |
                            +--------------------+    |
                            |                    |    |
                            +--------------------+ <--+
                low address       
    
    
    POP EBP
    
                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                EBP ------> |    previous EBP    |    |
                            +--------------------+    |
                            |  saved registers   |    |
                            +--------------------+    |
                            |       ......       |    |
                            +--------------------+    |
                            |        b=2         |    |
                            +--------------------+    | main()'s frame
                            |        a=1         |    |
                            +--------------------+    |
                            |         2          |    |
                            +--------------------+    |
                            |         1          |    |
                            +--------------------+    |
                ESP ------> |       0x122b       |    |
                            +--------------------+ <--+
                            |    previous EBP    |    |
                            +--------------------+    |
                            |        c=3         |    |
                            +--------------------+    | add()'s frame
                            |       ......       |    |
                            +--------------------+    |
                            |                    |    |
                            +--------------------+ <--+
                low address        
    
    

    然后执行 RET 指令,将返回地址赋给 EIP,从 add() 函数返回至 main() 函数,EIP 的值为 0x122b,即下一条指令为 main() 函数的 ADD ESP, 0x8

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                EBP ------> |    previous EBP    |    |
                            +--------------------+    |
                            |  saved registers   |    |
                            +--------------------+    |
                            |       ......       |    |
                            +--------------------+    |
                            |        b=2         |    | main()'s frame
                            +--------------------+    |
                            |        a=1         |    |
                            +--------------------+    |
                            |         2          |    |
                            +--------------------+    |
                ESP ------> |         1          |    |
                            +--------------------+ <--+
                low address       
    
    

    执行 ADD ESP, 0x8,清除 add() 函数的两个参数。

                            
                high address      
                            +--------------------+ <--+ 
                            |                    |    | 
                            |       ......       |    | previous frame
                            |                    |    |
                            +--------------------+ <--+ 
                EBP ------> |    previous EBP    |    |
                            +--------------------+    |
                            |   saved registers  |    |
                            +--------------------+    |
                            |       ......       |    | main()'s frame
                            +--------------------+    |
                            |        b=2         |    |
                            +--------------------+    |
                ESP ------> |        a=1         |    |
                            +--------------------+ <--+
                low address       
    
    

    至此 add() 函数的调用完成,可以在 main() 函数继续执行之后的指令了。

    系统调用

    应用程序无法调用内核函数,需要通过系统调用陷入到内核态,由操作系统执行。

    系统调用通过 INT 0x80 中断实现,不同的系统调用有不同的系统调用号,系统调用号需放在 EAX 寄存器中。

    系统调用号在 /usr/include/asm/unistd.h 中定义。

    查看 i386 体系结构下的系统调用号:

    陷入到内核态前需要保护现场,将寄存器、程序计数器压入内核栈,然后调用相应的内核函数(系统调用)。内核函数执行完成后,将返回值放入 EAX 寄存器,然后恢复现场,将寄存器、程序计数器从内核栈恢复,
    然后恢复到用户态。保护现场和恢复现场均由中断处理程序完成。

    系统调用参数传递与函数调用参数传递略有不同,当系统调用参数不超过 6 个时,参数从左到右放到寄存器 EBXECXEDXESIEDIEBP 中,如果参数超过 6 个,所有参数应该放在一块连续的内存区域里(C 结构体),用寄存器 EBX 保存指向该内存区域的指针。

    应用程序一般通过 API 去完成系统调用。API 与 系统调用 不同,一个 API 可能调用多个系统调用,不同的 API 也可能调用同一个系统调用。

    基本的流程可以概括为:

    • 应用程序调用 API
    • API 将系统调用号存入 EAX,然后通过中断调用系统调用
    • 中断处理程序保存寄存器至内核栈,陷入内核态,根据系统调用号调用对应的内核函数
    • 内核函数完成工作,保存返回值至 EAX
    • 中断处理程序从内核栈恢复寄存器,恢复到用户态,返回到 API
    • API 将 EAX 返回给应用程序

    系统调用的跟踪可用 strace 工具。

    参考

    CTF Linux pwn快速入门 - 哔哩哔哩

    Linux 内核分析 - 网易云课堂

    《Operating Systems: Three Easy Pieces》

  • 相关阅读:
    结构体初始化所遇到的问题
    字符串赋值注意事项
    C语言可变参数 "..."
    typedef 定义指针数组和数组指针及其使用。
    指针函数、指针数组、函数指针、数组指针、函数指针数组。
    前端回血day24 flex子项伤的CSS属性
    Fiddler使用
    一句话“截流”和“防抖”
    django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.
    Linux
  • 原文地址:https://www.cnblogs.com/wulitaotao/p/13898862.html
Copyright © 2011-2022 走看看