card_2-程序编译的流程, 动态库| 静态库| 源代码编译 | 汇编转cpp的理解

本文详细梳理了C++程序从编写到运行的整个流程,包括预处理、编译、汇编和链接。讨论了动态库和静态库的优缺点,强调源代码编译的可控性和安全性。同时,解释了CPU、内存、页表和调度算法的工作原理,以及汇编与C++代码的相互转换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

init

background

C++这门语言是一个追求底层的语言, 老实说我为什么选择C++就是因为它够底层, 让我能知道底层大致在干什么。 但是在学习的过程很明显存在不具体的问题, 而且C++语言的语法非常多,
是要做减法的。 基于这个背景, 我积累了一下以自己为中心的C++最佳实践和理解。

summary

知道程序从撰写到运行的过程。然后知道动态库|静态库|源代码各自的优缺点,明白源码编译才是最安全可控的。 重点理解一下汇编和cpp的转换, 明白cpp怎么执行汇编的。

c/c++程序编译和运行的流程梳理基本理解

  1. 程序编译到运行的过程是什么?
  • ·大致就是先通过complier解析语法树组合成汇编,然后进行链接,最后运行的时候就是将这个程序搞
    到内存中分配虚拟地址,然后开始cpu开始执行虚拟内存中程序的txt片中的指令,如果是计算指令就计算,操作数据的也会去对应的内存坎址中取数据,这些地址都是会通过页表映射到实际内存中的数据。https://zhuanlan.zhihu.com/p/547559531
  1. 为什么cpp编译那么慢?
  • .cpp编译这里我主要想说的C++为了兼容c,没有像其他语言使用模块导入,而是使用头文件将文本加载进来用编译器解析一下。但是这样非常麻烦的,可能会递归导入很多文件进来。此外在编译过程中做了很多指令集优化。
  1. 为什么其他语言感觉开源很多?
  • ·主要就是不用兼容c,可以直接将别人的库编译成元数据,不像cpp会把别人代码加载过来再解析一遍,cpp用别人的库,如果对方不通用化,成本还是很高的。

完整运行流程梳理

  1. 编译 :代码写完之后, 我们开始进行程序的编译;(感悟, 所有语言转换成可执行文件之后, 都是指令和数据了, 所以可以跨语言调用。)
    预处理(预处理如 #include、#define 等预编译指令的展开与解析, 生成 .i 或 .ii 文件)编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成 .s 文件)

  2. 汇编(汇编器把汇编码翻译成机器码,生成 .o 文件)。

  3. 链接( 这个过程第一阶段符号表解析 是把各个.o 文件的elf中同样段进行合并, 所有的符号表中und的变量都要在这过程中查找在哪, 解析成功之后给所有的符号分配虚拟地址。 , 最后生成 .out 文件 (注意这些过程根据objdump 查看elf文件) elf文件会包括了所有的信息。 可执行文件.out和.o大部分都相同, 但是可执行文件还有一个program 告诉系统哪些内容加载到内存中。 一般只加载代码段和数据段。

  4. 加载: 操作系统在装载应用程序时,主要分为三步:
    4.1. 为进程分配虚拟内存(逻辑内存),一个进程是4G, 分为txt , data ,bss(存储为0或者未初始化的全局变量) , heap, shared stack(从上往下增加的, 要删除一个必须把下面的都删除了, 因此没有内存碎片) kernel 。 动态库和mmap会加载到headp到stack之间。

  5. 2 建立虚拟内存与可执行文件的映射(在内核态的页表, 以4K大小为单位,将虚拟内存地址与可执行文件的偏移建立映射关系。形成一个虚拟地址到物理地址的页表,注意这里不是寻址的页表)

4.3. 将CPU的指令寄存器设置为应用程序的入口地址(入口地址在elf文件中),启动运行。

  1. 开始运行 :在完成3步后,程序开始执行。CPU在脉冲的操作下, 将程序指令地址读到PC指令寄存器,然后如果需要数据, cpu把数据通过地址总线读到存储寄存器中, 然后运算单元对数据进行处理, 处理完成继续程序指令寄存器。

6: 运行过程中堆栈调用过程:
栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表)。栈里面不仅有压栈出栈的函数调用, 还能分配局部变量。 这里面能讲解很深的~, 一定要注意。堆栈调用非常关键,深刻理解很有用。 这里面我简单说一下石磊老师课程里面的代码, 局部变量不产生地址, 直接根据栈的偏移量计算的。 直接是一个move指令。 调用函数中$ { $ 会存储之前栈的地址,开辟sum函数的栈空间, 保存之前栈的下一步汇编地址。 然后 执行函数的指令, 最后将存储return结果的形参变量内容交给一个寄存器。 右括号负责将这个调用栈回退(pop 得到之前的栈底的地址, 赋值给当前的栈底esp, 并将call function 的下一行指令直接给pc寄存器执行,(一般在这就回退到真正的main函数栈顶), 然后将存返回值的寄存器内容给main栈中的ret , 并开始从main栈顶继续执行。 在这过程中可能会反复发生调度算法切换,但是没事,不影响正常的程序调用。

7 : 运行过程中虚拟内存如何工作
当我们代码中访问到具体的数据时候,而不是只是申请虚拟内存的时候, 就会发生页表的实际分配。 接下来就让我们看看这里是怎么切换的。

内存管理单元(MMU)管理着虚拟地址空间和物理内存的转换,操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,存储着程序地址空间到物理内存空间的映射表。页表寻址中可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时(缓存不命中),会产生一次缺页异常加中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。如果执行出错那么就直接退出了。在mmu上查找虚拟地址的物理地址时候,如果内存满了等会做缺页置换算法。例如LRU, 总之可以不用做实际的转换, 直接将物理地址返回给cpu。

如果需要实际访问的话第一次没有缓存而且标志位为空的话会产生缺页异常,如果映射整体是三级表 , 就是经历(1)逻辑地址转线性地址, 这样我们拿到了32位的地址, 然后拆分10 ,10 ,12 大小分别找页表,页, 物理地址。还要注意缺页异常是前提, 随后触发中断, 去真正的将外存放到内存中,说明已经做了一次实际映射,这种就不是malloc , 而是实际的访问了。

8 : cpu的调度算法
每个进程的PCB都是存在所有进程共享的内核空间中,操作系统管理进程,也就是在内核空间中管理的,在内核空间中通过链表管理所有进程的PCB,如果有一个进程要被创建,实际上多分配了这么一个4G的虚拟内存,并在共享的内核空间中的双向链表中加入了自己的PCB。PCB(Process Control Block)进程控制块,描述进程的基本信息和运行状态。
还要注意, 在多道程序而且多用户的情况下,组织多个作业或任务时,就要解决处理器的进程调度。 如果CPU调度算法生效了, 需要进行程序寄存器上下文的保存, 之后再去调度其他的进程。常见的方法有先来先服务法等。

9: 优化性能

最后在整个阶段我们需要考虑几个优化方面的问题 :(1)内存碎片的避免, (2)如何进行优化核态和用户态, 库函数在这个过程中做了什么?

关于内存碎片的避免就是采用每天定时启动, 此外还有malloc底层使用了内存池。采用不同的链表绑定不同需求, 如果请求的大小在链表中满足直接返回, 不满足再去重新开辟。

第二个内核态是发生在一些文件读写或者事件响应中的, 内核态拥有最高权限,可以访问所有系统指令;用户态则只能访问一部分指令。一些对硬件操作或者重要的指令只有内核态才能访问到。例如:当读取文件等(read,write),需要进入内核态。 进入的方式就是软中断和硬件中断。 中断是当前程序需要暂停当前的任务去做的事情, 为了区分不同的中断,每个设备有自己的中断号。系统有0-255一共256个中断。系统有一张中断向量表,用于存放256个中断的中断服务程序入口地址。每个入口地址对应一段代码,即中断服务程序。 一般需要保存现场(当前的执行位置和当前状态到两个寄存器上), 模式切换, 找中断表中的函数, 执行函数, 返回恢复状态。

那么内核态和用户态的交互太多是非常影响性能的, 一般我们使用read, mmap.sendfile去调用内核, 但是不同的方法效率不一样,1. 调用read函数读取文件, 需要进入内核态再返回用户态。这里面拷贝过程比较多。2上面那个步骤可以使用mmap去避免一次拷贝。 kafka使用了这个机制做消息持久化, 它开辟了一个磁盘的mmap , 数据直接从用户态映射到磁盘。
3. 0用户态拷贝 是通过sendfile实现的, 就是说内核直接打开这个底层磁盘, 将数据直接通过内核态发送给客户端。

第三个库函数问题: 其实一些库函数在应用层添加了缓存区, 使用库函数调用可以大大减少系统调用的次数, 这个和系统的内核态还不一样。

动态库静态库| 源代码编译的理解

  1. 库和源码的编译区别? 顺序是啥?
  • ·区别很大,如果你依赖别人的库,要知道别人当时编译出来这个库的编译选项,不然你如果编译参数有问题,可能头文件展开会有问题。此外一般动态库编译运行速度慢,静态库编译的话如果你依赖的各种库链接的静态库因为版本不一致,函数定义不同,那么链接起来有版本问题,还要找源码进行修改。其实还不如源码全部下载到本地编译进来,速度还是非常可以的。
  • ·顺序就是先本地源码,然后再去编译库。
  1. 在编译器和标准库确定的条件下,动态库静态库和源码库有什么优缺点?
  • ·项目依赖动态库是不可控的,如果别人热加载升级动态库,你可能会不兼容,所以动态库很难测。
  • ·静态库发布也不行,如果别人的静态库依赖boost1.36,而你本地是1.40,,就冲突了,你必须用1.36.如果多几种这样的静态库,他说你必须用4.0,另外一个人说3.6,你这就是死循环了, 不像源码你可以修改成统一的。
  • ·源码编译才是王道:动态库别人修改你可以不在乎,主动权在自己手里,而且编译选项集体统一。而且也解决了静态库冲突,因为有冲突,你可以自己修改他们的源码实现。
  1. 1.动态库的二进制兼容性是啥?有什么问题?
  • ·如果动态库的代码改了,影响别人的调用了,就不符合二进制兼容,例如你改了函数变为虚函数。
    有些操作兼容,有些不兼容,具体参照muduo库的书。

计算机组成原理相关知识

理解cpu中间有寄存器缓存和运算寄存器等等,然后再访问内存的。
请问CPU,内核,寄存器,缓存,RAM,ROM的作用和他们之间的联系?-温戈的回答一知乎https://www.zhihu.com/question/24565362/answer/2288686350

汇编代码和C++互转的理解

理解cpu中间有寄存器缓存和运算寄存器等等,然后再访问内存的,一些基本的指令要回,例如mov啥的。

basic knowledge

参照汇编指令—用GDB调试汇编-不雨的文章-知乎
https://zhuanlan.zhihu.com/p/259625135 这个讲的很好了,但是并不是逐条讲的,所以需要看https://mp.weixin.qq.com/s/qrNjN3v0qe5AkV9TOhCxFw

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值