C/C++如何运行起来,以及在内存的分布情况

C/C++ Restart ➢ 1.0

Question:编写的C++代码如何运行起来的?如下述的从左到右的顺序
编写源代码预处理(Preprocessing)编译(Compilation)汇编(Assembly)链接(Linking)装入(Loading)执行()
  • 编写源代码:高级程序语言的编写,如遵循C/C++语法规则正确的编写好代码
  • 预处理:编译前经过预处理器来处理头文件和宏展开来生成称为"翻译单元"的中间代码
  • 编译:预处理后的代码送入编译器。编译器将源代码翻译成汇编代码。
  • 汇编:汇编器将汇编代码翻译成机器代码或目标代码,生成可执行文件。
  • 链接:如果程序包含多个源文件,编译器会生成多个目标文件,链接器将这些目标文件和可能的库文件组合成一个可执行文件。
  • 装入:操作系统将可执行文件加载到内存中,并将控制权交给程序的入口点,通常是main函数。
  • 执行:按照源代码中定义的逻辑执行相应的操作。

预处理

预处理器的工作由预处理器指令(以#开头的指令)控制,这些指令会在编译前先对源代码进行修改。

#include <iostream>	//用于包含头文件,将指定的文件内容插入到当前文件中。
#define PI 3.14159 //用于定义宏,预处理阶段把所有PI替换为3.13159
#define SQUARE(x) ((x) * (x)) //宏展开替换的例子
int num = 5;
//预处理前
int result = SQUARE(num);
//预处理后
int result = ((num) * (num));
#define _TEST_//定义宏
// ...
#undef _TEST_//取消宏
//#ifdef、#endif: 用于条件编译,根据是否定义了指定的宏来包含或排除代码块。
#ifdef _TEST_
    //如果定义了宏_TEST_,则这一部分保留,如果没定义则这一部分抛弃
#endif
//#if、#else、#endif: 用于条件编译,根据指定的条件包含或排除代码块。同上
#if defined(_TEST_)
    //
#else
    //
#endif
#pragma once//提供一种向编译器发出特定命令的方式,通常用于控制编译器的行为。
//#error: 用于在预处理阶段生成编译错误,可用于强制程序员注意或在特定条件下中断编译。
#ifdef LINUX
    // Linux-specific code
#else
    #error "Unsupported platform"
#endif

TODO:#pragma once补充说明示例

编译

在这里插入图片描述

  • 词法分析器:又称扫描器,输入源程序,进行词法分析,输出单词符号。

  • 语法分析器,又称分析器,对单词符号串进行语法分析,识别出各类语法单位,最终判断输入串是否构成语法上正确的“程序”。

  • 语义分析与中间代码生成器:按照语义规则对语法分析器归约(或推导)出的语法单位进行语义分析,并把它们翻译成一定形式的中间代码。

  • 优化器:对中间代码进行优化处理。

  • 目标代码生成器:把中间代码翻译成目标代码。

表格管理:用于登记源程序的各类信息和编译各阶段的进展状况。
出错处理的作用:最大限度的发现各种错误。准确指出错误的性质和发生错误的地点。将错误所造成的影响限制在尽可能小的范围,以便使得源程序的其余部分可以继续被编译下去,以进一步发现其它可能的错误。 如果可能,自动校正错误。

Question:什么是中间代码,什么目标代码?

中间代码:中间代码是一种介于源代码和目标代码之间的抽象表示形式。在编译过程中,源代码首先被转换成中间代码,这种代码通常更容易进行优化和跨平台移植。如果不生成中间代码而是直接生成机器语言或者汇编语言形式的目标代码呢?优点是编译时间短,缺点是目标代码执行效率和质量都比较低,移植性差。

表现形式如下(简单了解)

  • 后缀式(逆波兰式)
  • 图表示法
  • 三地址代码

目标代码:源程序的中间代码作为输入,产生等价的目标程序作为输出;目标代码有可能的三种形式

  • 能够立即执行的机器语言代码,所有地址均已定位。
  • 待装配的机器语言模块。
  • 汇编语言代码,则后续需经过汇编程序汇编,转换为可执行的机器语言代码。

【汇编代码文件Windows环境下通常为.asm文件,Linux环境下为.s文件】

IDE(集成开发环境):IDE是一个集成了编辑器、编译器和调试器等工具的软件。

C/C++常见的编译器
  • GCC(GNU Compiler Collection):一个开源的编译器集合,支持多种编程语言,包括C和C++。在许多Linux和Unix系统上默认使用。
  • Clang:由LLVM项目开发的编译器,支持C、C++等语言。Clang具有快速的编译速度和先进的诊断能力。
  • MSVC(Microsoft Visual C++ Compiler):Microsoft Visual Studio集成的编译器,用于在Windows平台上编译C++代码。
C/C++常见的调试器
  • GDB(GNU Debugger):一个功能强大的开源调试器,适用于多种编程语言,包括C和C++。与GCC集成,常用于Linux系统。
  • LLDB:由LLVM项目开发的调试器,提供类似于GDB的功能,但具有更现代的设计。常用于macOS系统。
  • Visual Studio Debugger:Microsoft Visual Studio集成的调试器,适用于在Windows平台上开发和调试C++代码。
  • Xcode Debugger:Xcode是苹果公司提供的集成开发环境,用于开发macOS和iOS应用程序。它包含了一个调试器,可用于调试Objective-C和C++代码。

汇编

汇编器将汇编代码转换成机器码机器语言,并生成目标文件Linux下的.o 文件(Window环境下为.obj文件)。可以称之为目标模块

TODO:汇编代码的学习要看懂

链接

由链接器链接程序把汇编后的一组目标模块,以及所需要的库函数链接一起,形成一个完整的装入模块(装入模块也就是*.exe文件)

链接的三种方式

  1. 静态链接:运行前,把所有各个目标模块和包含的库函数链接成一个完整的装入模块(可执行文件)之后不再拆开。确认好了完整的逻辑地址、
  2. 装入时动态链接:装入时,将各个目标模块装入内存时,边装入边链接
  3. 运行时动态链接:运行时,需要的目标模块才对它链接,优点便于修改和更新,便于实现目标模块的共享。

装入

由装入程序把装入模块装入内存运行

进程运行的基本原理

  • 指令工作原理

eg:代码x=x+2;会经过编译转为指令集,假设现在的编译出来的指令集共有3条指令(机器指令)

分别是操作码(干什么操作),地址码(去哪里取被操作数),数据(操作的数据)

即如下,CPU通过操作码00010010知道要执行对地址码01010011的变量x加上数据00000010后返回到地址码01010011。【假设】

操作码00010010地址码01010011数据00000010

进程在内存由三部分组成,程序段、数据段、PCB。指令存在于程序段,x数据在数据段。内部维护的按照程序计数器从头开始从上往下顺序给CPU执行指令。但是这一切的指令地址是相对于进程自己的。即理解为此时0000 0000 ~ 1111 1111的逻辑地址都是内存自己知道的。但是放到内存就不知道了。假设内存4G = 232B;

即 0000 0000 0000 0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 1111 1111 1111都是内存的实际物理地址

假如此时放到了0000 0000 0000 ~ 0000 1111 1111的物理地址,这样本地指令编译出来的地址码操作码的地址都是错误的,因为参照对象是0000 0000 ~ 1111 1111。

attention:进程起始地址的设定为0,可是不一定放到内存的地址就是从0开始。

  • 逻辑地址和物理地址

逻辑地址(相对地址):我们都会设定一个进程的地址是从0开始的,而逻辑地址就是相对于进程其实地址而言的地址。(可以理解进程中的偏移地址)

物理地址(绝对地址):相对于内存而言的地址。

如何实现地址转换

如何把指令中的逻辑(相对)地址转换为物理(绝对)地址?策略为三种装入方式

  • 绝对装入:编译时,事先知道会放在内存哪里,编译程序就可以产生拥有绝对地址的目标代码,再链接到装入模块,装入程序也按照装入模块的目标代码的地址放对。(编译程序时
    • 因为编译器的知道放内存哪里,在我的电脑写出来的可以,在别人电脑不一定可以。灵活性低,只适合单道程序环境
  • 可重定位装入(静态重定位):编译,链接到装入模块还是逻辑地址,当装入到内存时,对地址"重定位"即逻辑地址转换为物理地址,装入时一次完成。(装入程序装入内存时
    • 由于这是一次装入完成,必须作业装入内存时,要分配好全部空间,如不够则不能装入该作业,一旦作业装入内存,运行期间不能移动,也不能再申请新空间。因为物理地址写死了
  • 动态运行装入(动态重定位):(现代操作系统)编译,链接到装入模块,装入内存后都是逻辑地址。只有程序真正执行才去地址转换。需要一个重定位寄存器支持。(运行时)
    • 基址/重定位寄存器存放装入模块放到内存的起始地址(理解为记录了程序的绝对地址),即程序要执行逻辑地址79的指令,重定位寄存器为100,即程序从内存100开始存的,即指令在内存的179位置。
    • 运行程序在内存移动,也可以分配不连续的存储区,可以先装入部分代码,根据需要动态申请内存和放入【扯到虚拟存储管理】
Question:C/C++运行时的内存分区

一个C/C++程序放入内存的执行过程可以理解为进程。进程分PCB和数据段和数据段,与内存四区的关系如下

代码段:

  • 代码区

数据段:

  • 全局静态区
    • bss区段:全局未初始化、静态未初始化的数据
    • data区段:全局初始化、静态初始化的数据
    • 常量区段:常量数据
  • 堆区
  • 栈区

文件映射区:它不同于传统的代码段和数据段的划分,它提供了一种在虚拟地址空间中与文件进行关联的机制。这种映射通常通过操作系统提供的mmap(在类Unix系统上)或MapViewOfFile(在Windows上)等函数实现。文件映射区通常位于进程的虚拟地址空间,允许进程直接访问文件内容,而无需显式地调用读取或写入函数。

高地址0xFFFF FFFF
栈区
文件映射区
堆区
bss
data
rodata常量
代码段
低地址0x0000 0000
堆区栈区
管理方式堆中资源由程序员控制(容易产生memory leak)栈资源由编译器自动管理,无需手工控制
空间大小非连续(系统是用链表来存储空闲内存地址)大小受限于虚拟内存大小(32bit 系统理论上是比4G小一些些),通常较大栈是一块连续的内存区域,大小是操作系统预定好的1M或设置,较小
生长方向堆向上,向高地址方向增长。栈向下,向低地址方向增长。
分配方式堆都是动态分配(没有静态分配的堆)栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca 函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现!
分配效率堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多。栈是其系统提供的数据结构, 计算机在底层对栈提供支持, 分配专门 寄存器存放栈地址, 栈操作有专门指令。
碎片问题对于堆,频繁的new/delete会造成大量碎片,使程序效率降低对于栈,有点类似于数据结构上的一个先进后出的栈, 进出一一对应,不会产生碎片。
内存管理机制系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中);只要栈的剩余空间大于所申请空间,系统为程序提供内存, 否则报异常提示栈溢出。(这一块理解一下链表和队列的区别,不连续空间和连续空间的区别,应该就比较好理解这两种机制的区别了)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值