【编译基础】程序从出生到入土

函数调用序,一文即可知

程序静态内存模型
程序动态内存模型
堆和栈的区别
函数调用过程
main函数中形参的含义
函数调用

😊点此到文末惊喜↩︎

概述

程序编译执行的流程

  1. 预编译
    • 处理预编译指令(按照程序文件顺序)
      • #ifndef、#define、#endif:避免头文件的逻辑重复包含
      • 保留#pragma:指示编译器如何优化代码
      • 递归替换#include:将头文件内容插入当前文件中
      • 删除#define:展开所有宏定义
      • #ifdef、#endif:调试代码的包含,防御性编程
    • 删除所有注释,并添加行号和文件标识
  2. 编译
    • 词法分析:输入组成源程序的字符流划分为词法单元流
    • 语法分析:利用词法单元流构建一颗语法树
    • 语义分析:使用语法树和符号表中的数据,进行类型和运算符的匹配检查
    • 中间代码生成:生成易于翻译到各种不同平台的机器无关语言
    • 中间代码优化:期望使用更长的编译时间换取更高效的目标程序运行,主要是状态机的等价简化
    • 机器代码生成:使用中间代码生成目标机器语言
    • 机器代码优化:生成更高效的目标机器语言
  3. 汇编
    • 目标机器语言生成可重定位的二进制目标程序
  4. 链接(类似inline)
    • 静态链接:将程序所包含的库文件全部拷贝到可执行文件中
      • 优点:运行速度快,删除静态库不影响代码执行。
      • 缺点:库函数修改需要重新链接,同一个链接文件可能在多个可执行程序中都有副本,浪费空间
    • 动态链接:将程序分成独立模块,在执行时再链接形成可执行文件
      • 优点:所有程序共享库文件,节省内存空间。单个文件修改不影响整体执行,节省编译时间
      • 缺点:每次执行时进行链接会产生性能损耗
  5. 加载
    • 整体过程:磁盘上的ELF文件通过加载器,加载到内存中成为进程
      • ELF:可执行可链接格式文件,是磁盘上的静态程序
      • 加载器:调用execve,进行ELF文件进行解析并复制到对应内存段
      • 进程:内存中运行的程序的动态映像

内存模型

  1. ELF可执行目标文件(存储在磁盘上的静态程序
    • 只读代码段
      • ELF头部:描述文件总体格式
      • 段头部表:描述ELF文件和内存的映射关系
      • .init段:程序运行环境的初始化函数
      • .text段:存放已编译的程序二进制码
      • .rodata段:存放只读常量,如字符串、switch的跳转表、const修饰的全局变量···
    • 读/写数据段
      • .data段:存储已初始化(非零)的全局变量和静态变量,占用可执行文件空间
      • .bss段:存放程序中未初始化的或者初始化为0的全局变量和静态变量,但是在可执行文件中只简单维护每个变量起始地址和大小,在运行时才由操作系统初始化(eg:位图的数据结构也是使用首地址+大小进行索引)
    • 不加载到内存的符号表调试信息 在这里插入图片描述
  2. 程序动态内存模型/进程虚拟内存结构(进程是程序在内存的一次动态执行)
    • .text:只读代码段
    • .data:已初始化数据段
    • 运行时堆:通过malloc分配,每个进程维护一个指向堆顶的brk(由库函数sbrk通过系统调用brk调整堆内存断点——break地址,简称brk)
    • 共享库的内存映射区:多个进程分别将物理内存的共享库文件通过mmap映射进自己的虚拟地址空间,当有进程写该区域会引发写时复制,会在物理内存创建该页面的副本,然后更新写进程页表获得真正的写权限
    • 用户栈:存储函数调用过程,自高向低生长(害怕栈溢出破坏内核)
    • 内核区:进程的唯一标识,进程控制块PCB(在linux中为task_struct)
      在这里插入图片描述
  3. 其他注意事项
    • 进程是程序的一次动态运行,静态的程序内存模型,载入内存执行时
      • .init段负责程序环境的初始化,在加载程序时就执行完成
      • .bss:运行时会被初始化到对应的数据段中
      • .rodata段通常直接被编译到指令中,存放在代码段中
    • 为什么程序动态内存模型中没有.rodata段
      • 直接编译到代码段(.text)中,所以只读数据尽量使用const修饰,效率高。
      • 加载到共享库段,可以让多个进程共享,提高空间利用率
      • 在有的嵌入式系统中,将.rodata 放在ROM,运行时通过XIP(就地执行)直接读取,而无需要加载到RAM内存中。
      • 对于字符串常量,编译器会自动去掉重复的字符串,保证一个字符串在一个可执行文件(EXE/SO)中只存在一份拷贝。
    • 使用局部变量时,尽量进行初始化。编译器不会初始化局部变量,所以如果使用未初始化的局部变量,内部的值是垃圾值。但是debug调试模式下,运行时会将栈空间全部初始化为0

栈和堆

  1. 区别
    • 管理方式不同:栈由操作系统自动释放。堆的申请和释放工作由程序员控制,容易产生内存泄漏(通过智能指针解决
    • 空间大小不同:通常每个进程的栈空间远远小于堆空间
    • 生长方式不同:堆在内存中由低地址向高地址生长,栈在内存中由高地址向低地址生长,因为高地址是内核。
    • 分配方式不同:堆都是动态分配的。栈有 2 种分配方式:静态分配和动态分配。静态分配是由操作系统完成的。动态分配由alloca()函数分配,由操作系统自动释放。
    • 执行效率不同:
      • 栈由操作系统自动分配,有硬件级支持:有专用的寄存器和出入栈指令
      • 堆则由较复杂内存管理算法分配(伙伴算法、slab小对象优化),可能产生内存碎片和缺页中断。
    • 存放内容不同。
      • 栈,存放函数的调用过程。主要是函数返回地址、局部变量和寄存器内容等
      • 堆,具体存放内容由程序员来填充的。
  2. 堆概述
    • 定义:堆是动态内存分配器维护的进程虚拟内存空间,内核为每个进程维护一个指向堆顶的brk指针
    • 申请内存:malloc使用内存池来管理内存。当程序调用malloc申请内存时,malloc会通过系统调用向操作系统申请一段虚拟内存(逻辑上标记为已分配)。当程序去访问这段虚拟内存时,如果相应的物理页没有被装载到内存中,就会触发缺页异常Page Faults。此时操作系统会负责将相应的物理页从硬盘中装载到内存中,并将页表进行更新,使得该虚拟地址能够映射到正确的物理地址上。
      • 当申请小内存时,使用系统调用sbrk进行内存分配
      • 当申请大内存时,使用系统调用mmap进行内存分配
    • 释放内存:分配器将堆视为一组不同大小的块(block)的集合来维护。每个块是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的
      • 显式分配器:要求程序显式地释放任何已经分配的块,eg:free函数
      • 隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。也称垃圾收集器,eg:java
  3. 操作系统栈空间大小
    • Linux下是由系统环境变量来控制栈的大小的,默认大小为8MB
    $ ulimit -a            # 显示当前栈的大小 (ulimit为系统命令,非编译器命令)       
    $ ulimit -s 32768      # 设置当前栈的大小为32M
    
    • windows下可以由编译器决定栈大小,VC++ 6.0 默认的栈空间是1M

函数调用过程

  1. 时间紧迫就不要看,【 call 计算机从开机加电到执行main函数之前的过程】如果看完了这篇文章,别忘了ret返回这里,哈哈哈
  2. 任何的C或C++程序,最开始执行的是启动码函数mainCRTStartup(),而启动码函数初始化完程序的运行环境后,最终由invoke_main()调用main函数
  3. 构造主函数栈帧(每个函数实际执行前都要进行)
  4. call 子函数名,即主函数调用子函数
    • push ip:将主函数中调用点的下一条指令地址压入栈中
    • jmp [子函数]:跳转到函数入口地址表的对应函数,再跳转到代码段中的函数执行
  5. push ebp,将主函数的栈底指针压入栈中
  6. mov ebp esp,使栈底指针指向栈顶,即构造子函数的栈底
  7. 栈顶指针esp下移留出局部变量存储内存空间
  8. 压入caller-save register(IA32为ebx,esi,edi),用于保存函数的处理信息
  9. 初始化栈底ebp指向地址和存储ebx的地址之间的内存空间
  10. 执行子函数
  11. caller-save register value逆序弹出到对应寄存器中
  12. mov esp ebp ,用子函数的ebp给esp赋值,将栈顶指针指向栈底
  13. pop ebp 将栈顶的上一个函数的ebp值弹出到ebp寄存器中
  14. ret 将栈顶的值(主函数的调用断点的下一条指令的地址)弹出到EIP中,EIP会执行下一条指令,即返回主函数。
  • 其中call 下一条地址属于主函数的状态,属于主函数栈帧。而ebp属于子函数的返回地址,属于子函数栈帧(csapp第三版p165)
  • 函数栈帧:返回地址(主调函数的栈底指针)、局部变量、caller-save Aregister
    在这里插入图片描述

main函数的形参含义

  1. mian函数参数的含义是什么
    • argc:参数计数器,整型变量 ,表示参数的个数.
    • argv:字符串指针的数组,第0个为可执行程序文件目录位置,其后为该命令的参数
    int main(int argc, char* argv[]){
        int i;
    	for (i = 0; i<argc; i++)
    		cout<<argv[i]<<endl;
        cout<<argc;
    	return 0;
    }
    //输入:
    	F:\MYDOCU~1\TEMPCODE\D1\DEBUG\D1.EXE aaaa bbb ccc ddd
    //输出:
    	F:\MYDOCU~1\TEMPCODE\D1\DEBUG\D1.EXE
    	aaaa
    	bbb
    	ccc
    	ddd 
    	5
    

C调用汇编语言

  1. 不同文件中的代码
// C文件main.c
extern void print(char*, int); // 表示print函数不在本文件内,使用extern声明
int main(void)
{
    print("hello\n", 6);
    return 0;
}

; 汇编文件:print.asm
global print 				; 设置print为全局可见
print:
    push ebp
    mov ebp, esp
    mov eax, 4              ; 发起系统调用
    mov ebx, 1              ; ebx表示stdout
    mov ecx, [ebp + 8]      ; 取出传入的第二个参数,表示字符串的地址
    mov edx, [ebp + 12]     ; 取出传入的第一个参数,表示字符的个数
    int 0x80                ; int 0x80表示系统调用

    pop ebp
    ret


反汇编演示

  1. 编译环境:vs2022&win10
  2. 编译器问题:编译器越高级,其中处理的越会繁琐。在不同的编译器下,函数的调用过程中的栈帧的创建是略有差异的,具体细节取决于编译器的实现
  3. 前置知识
    • 程序寄存器组是所有函数调用过程的共享资源。所以函数调用需要将当前函数执行相关的寄存器值保存到函数自己的栈帧中,当函数返回时,再将函数栈帧中的值恢复到相应的寄存器中。IA32采用于一组统一的寄存器使用惯例:寄存器eax,edx,ecx被分为调用者的可用寄存器,即当过程P调用Q时,Q可以覆盖这些寄存器,而不会破坏任何P所需要的数据。另外,ebx,esi,edi分为被调用者保存寄存器,意味着覆盖他们之前,将这些寄存器的值保存在栈中,并在返回前恢复他们。
    • 调用函数所做的工作:将当前的指令的下一条指令的地址保存,保存的目的是为了调用结束后修改PC值返回,然后跳转至目标地址处。实现跳转是由修改EIP(PC)的值完成的。
    • 任何一个临时变量都保存在当前的函数的栈帧内。调用结束后,修改esp和ebp完成空间释放,但栈帧实际还存在,只是告诉编译器这部分栈空间可以被覆盖掉。文件删除也是这个原理。
    • return所做的工作是将当前的函数的返回值地址出栈,利用pop的数据修改EIP。
    • 调用函数的空间时间开销主要来自于栈帧的开辟与释放。每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间
    • 函数的返回值时通过寄存器进行保存和返回的
  4. 32位下反汇编(下面从0开始)
// 被调用的子函数
int sum(int a, int b) {
	// 5. 构造子函数栈帧
00451740  push        ebp  //把ebp寄存器的值入栈,此时的ebp中存放的是主调函数的栈基址
00451741  mov         ebp,esp// 将当前栈顶指针esp赋值给栈基址寄存器ebp,即现在为子函数栈帧
00451743  sub         esp,0CCh// 栈顶指针esp下移(栈由高向低生长),即由esp和ebp共同维护这一段子函数栈帧
00451749  push        ebx  //将寄存器ebx的值压栈,esp-4
0045174A  push        esi  //将寄存器esi的值压栈,esp-4
0045174B  push        edi  //将寄存器edi的值压栈,esp-4
0045174C  lea         edi,[ebp-0Ch]  //先把ebp-0E4h的地址,放在edi中
// 下三行将ebp指向的内存到值为ebx之间初始化成0CCCCCCCCh  
0045174F  mov         ecx,3  
00451754  mov         eax,0CCCCCCCCh  
00451759  rep stos    dword ptr es:[edi]  
// 下两行为编译器debug模式下调试用的cookie
0045175B  mov         ecx,offset _01833B24_TestCpp@cpp (045C008h)  
00451760  call        @__CheckForDebuggerJustMyCode@4 (045130Ch)  
	// 6. 执行子函数功能
	int c = a + b;
00451765  mov         eax,dword ptr [a]  
00451768  add         eax,dword ptr [b]  
0045176B  mov         dword ptr [c],eax  
	return c;
// 返回值放入eax中,函数调用返回时不会被覆盖
0045176E  mov         eax,dword ptr [c]  
}
00451771  pop         edi  //在栈顶弹出一个值,存放到edi中,esp+4
00451772  pop         esi  //在栈顶弹出一个值,存放到esi中,esp+4
00451773  pop         ebx  //在栈顶弹出一个值,存放到ebx中,esp+4
// 下三行为debug模式下的cookie
00451774  add         esp,0CCh  
0045177A  cmp         ebp,esp  
0045177C  call        __RTC_CheckEsp (0451235h)  
	// 7. 将栈帧恢复为主函数的栈帧(ebp和esp)
00451781  mov         esp,ebp // 将子函数的栈底指针赋值给栈顶指针esp,相当于回收栈,但是子函数栈帧仍然存在栈中 
00451783  pop         ebp  //弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈底。
	// 8. 返回到主函数的调用点的下一条指令的地址
00451784  ret  //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行
···
// 主函数
int main() {
	// 0. 构造主函数栈帧(配合堆栈结构图片看更容易理解)
004517B0  push        ebp  //把ebp寄存器的值入栈,此时的ebp中存放的是invoke_main栈基址
004517B1  mov         ebp,esp // 将当前栈顶指针esp赋值给栈基址寄存器ebp,即现在为mian函数栈帧
004517B3  sub         esp,0D8h// 栈顶指针esp下移(栈由高向低生长),即由esp和ebp共同维护这一段mian栈帧  
004517B9  push        ebx //将寄存器ebx的值压栈,esp-4,这三个应该是main的三个形参变量 
004517BA  push        esi //将寄存器esi的值压栈,esp-4 
004517BB  push        edi //将寄存器edi的值压栈,esp-4 
004517BC  lea         edi,[ebp-18h]  //先把ebp-18h的地址,放在edi中
// 下三行将ebp指向的内存到值为ebx之间初始化成0CCCCCCCCh  
004517BF  mov         ecx,6  
004517C4  mov         eax,0CCCCCCCCh  
004517C9  rep stos    dword ptr es:[edi]
// 下两行为编译器debug模式下调试用的cookie  
004517CB  mov         ecx,offset _01833B24_TestCpp@cpp (045C008h)  
004517D0  call        @__CheckForDebuggerJustMyCode@4 (045130Ch)  
// 函数功能实现
	// 1. 为main函数栈帧中的局部变量赋值(系统啥时候把a和b初始化成main堆栈的?)
	int a = 1;
004517D5  mov         dword ptr [a],1  // a相当于一个指针,将1赋值到a指向的内存中,实际在mian函数的栈帧中
	int b = 2;
004517DC  mov         dword ptr [b],2  // 同上
	sum(a, b);
	// 2. 将被调用函数的参数从右向左依次用通用寄存器保存
004517E3  mov         eax,dword ptr [b]  
004517E6  push        eax  
004517E7  mov         ecx,dword ptr [a]  
004517EA  push        ecx  
	// 3. call子函数,分成两步,1.将程序下一条指令地址压入栈中2.转移到调用的子函数(最后一行)
004517EB  call        sum (045116Dh)  
004517F0  add         esp,8  
	return 0;
004517F3  xor         eax,eax  
}
004517F5  pop         edi  
004517F6  pop         esi  
004517F7  pop         ebx  
004517F8  add         esp,0D8h  
004517FE  cmp         ebp,esp  
00451800  call        __RTC_CheckEsp (0451235h)  
00451805  mov         esp,ebp  
00451807  pop         ebp  
00451808  ret  
// 以下是函数表,只是一个中转作用
···
	// 4. 执行跳转指令到子函数的执行(第一行)
0045116D  jmp         sum (0451740h)
···  

在这里插入图片描述
5. 64位下反汇编,代码没问题,但是我的注释可能有问题,有的地方没理解,等我神功大成


#include <stdio.h>
// 主函数调用的子函数
int sum(int a, int b) {
	// 4. 将子函数的参数自右向左依次压入栈中
00007FF7F5631740  mov         dword ptr [rsp+10h],edx // 栈由高地址向低地址生长
00007FF7F5631744  mov         dword ptr [rsp+8],ecx  // 这是低地址
	// 5. 压入主调函数的栈基址,即上一个栈帧的开始地址
00007FF7F5631748  push        rbp  
	// 6.将主调函数的函数调用点的下一条指针地址压入栈中
00007FF7F5631749  push        rdi  
00007FF7F563174A  sub         rsp,108h  
00007FF7F5631751  lea         rbp,[rsp+20h]  
00007FF7F5631756  lea         rcx,[__01833B24_TestCpp@cpp (07FF7F5641008h)]  
00007FF7F563175D  call        __CheckForDebuggerJustMyCode (07FF7F5631343h)  
	// 7. 执行函数体内的功能语句
	int c = a + b;
00007FF7F5631762  mov         eax,dword ptr [b]  
00007FF7F5631768  mov         ecx,dword ptr [a]  
00007FF7F563176E  add         ecx,eax  
00007FF7F5631770  mov         eax,ecx  
00007FF7F5631772  mov         dword ptr [c],eax  
	return c;
00007FF7F5631775  mov         eax,dword ptr [c]  // 将返回值赋值到eax中
}
00007FF7F5631778  lea         rsp,[rbp+0E8h]
	// 8. 注意此时栈顶依次为rdi rbp。所以逆序pop到相应的寄存器中
00007FF7F563177F  pop         rdi  
00007FF7F5631780  pop         rbp  
	// 9. ret是子函数返回指令,与call搭配使用,修改pc(存储下一条将要执行的指令地址),并恢复主函数堆栈
00007FF7F5631781  ret  
// main函数
int main() {
	// 0. debug模式下的插入的cookie?
00007FF7F56317A0  push        rbp  
00007FF7F56317A2  push        rdi  
00007FF7F56317A3  sub         rsp,128h  
00007FF7F56317AA  lea         rbp,[rsp+20h]  
00007FF7F56317AF  lea         rcx,[__01833B24_TestCpp@cpp (07FF7F5641008h)]  
00007FF7F56317B6  call        __CheckForDebuggerJustMyCode (07FF7F5631343h)  
	// 1. 分别开辟4个字节大小的双字内存并将值移入
	int a = 1;
00007FF7F56317BB  mov         dword ptr [a],1  
	int b = 2;
00007FF7F56317C2  mov         dword ptr [b],2  
	// 2. 将参数压入寄存器中,调用子函数
	sum(a, b);
00007FF7F56317C9  mov         edx,dword ptr [b]  // 将内存地址为a的双字类型的数据赋值给edx
00007FF7F56317CC  mov         ecx,dword ptr [a]  
00007FF7F56317CF  call        sum (07FF7F56313A2h)  // 调用子函数(最后一行)
	return 0;
00007FF7F56317D4  xor         eax,eax  
}
00007FF7F56317D6  lea         rsp,[rbp+108h]  
00007FF7F56317DD  pop         rdi  
00007FF7F56317DE  pop         rbp  
00007FF7F56317DF  ret  

// 从函数表中截取的子函数跳转指令
	// 3. 跳转到子函数
00007FF7F56313A2  jmp         sum (07FF7F5631740h)  // 第一行
···

栈溢出实验

  1. 【深度补充】这样还学不会栈溢出的小伙伴麻烦私信我https://www.bilibili.com/video/BV1QV411r7UU/?spm_id_from=333.337.search-card.all.click&vd_source=ce626ff62ed6a7b65ff163189a520fb1
  2. 两个栈溢出的CVE漏洞实验https://blog.csdn.net/qq_43840665/article/details/124265725


少年,我观你骨骼清奇,颖悟绝伦,必成人中龙凤。
秘籍(点击图中书籍)·有缘·赠予你


🚩点此跳转到首行↩︎

参考博客

  1. 侯捷——c++的生前死后
  2. 一文读懂 .bss段 的作用
  3. 简述代码中关于.data、.bss、.rodata、.text段的意义
  4. 一文读懂堆与栈的区别
  5. 《C语言》函数栈帧的创建与销毁–(内功)
  6. 函数调用的执行过程
  7. 函数调用过程中函数栈详解
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逆羽飘扬

如果有用,请支持一下。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值