【施磊】C++语言基础提高:深入学习C++语言先要练好的内功


课程总目录


文章目录

  • 一、进程的虚拟地址空间内存划分和布局
  • 二、函数的调用堆栈详细过程
  • 三、程序编译链接原理
    • 1. 编译过程
    • 2. 链接过程


一、进程的虚拟地址空间内存划分和布局

任何的编程语言 → \to 产生两种东西:指令和数据

编译链接完成之后会产生一个可执行文件xxx.exe,会把程序从磁盘加载到内存中,不可能直接加载到物理内存!!!

环境: x86 32位linux环境

程序:

int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main()
{
	int a = 12;
	int b = 0;
	int c;
	static int e = 13;
	static int f = 0;
	static int g;

	return 0;
}

linux系统会给当前进程分配一个 232(4G)大小的一块空间(进程的虚拟地址空间),大小和环境的位数相关,如果是64位,则为8G

在这里插入图片描述

注意区分虚拟内存虚拟地址空间,这是两个不同的概念

  1. 0x00000000 ~ 0x08048000
    这段无法被访问,如果运行char *p = nullptr;strlen(p);则会报错,因为空指针在这段区域,char *src = nullptr;strcpy(dest, src);也会报错

  2. 0x08048000 ~ 0xC0000000

    • .text(代码段): 放指令只读)。main函数中的三个初始化 a, b, c 语句,都会转化为一条mov指令,如mov dword ptr[a], 0xCH,如果cout << c,此时的c是什么不确定(参考文章),它是栈上的无效值;int main(){}以及cout << c << g << endl;都是指令,都存放在 .text

    int a = 12; 这条语句不产生符号,只产生对应的汇编指令,对应指令存放在 .text上,但是当指令运行的时候,指令做的是在栈上开辟4字节的空间将12放进去

    • .rodata: 只读数据read only。char *p = "hello world";其中p在栈上,常量字符串"hello world"就存储在 .rodata段,但是如果*p = 'a';,通过指针让常量字符串的第一个字符修改为a,可以编译但不能运行,因为这一部分是只读的
    • .data(数据段): 用于存储已经初始化并且不为0全局变量和静态变量,这些变量在程序运行之初就有了确定的初始值,在程序执行之前就会被初始化,因此需要分配实际的存储空间。 [gdata1 & gdata4 & e]
    • .bss: 用于存储未初始化和已经初始化为0全局变量和静态变量[gdata2 & gdata3 & gdata5 & gdata6 & f & g]

    此时cout << gdata3 << endl;输出为0,因为gdata3存放在 .bss段。操作系统会把没初始化的变量全部置为0

    • .heap:堆
    • 加载共享库:在window系统中是*.dll,在linux中是*.so
    • stack:栈,函数运行或产生线程时,产生的栈空间,从下往上(高地址向地地址)进行增长
    • 命令行参数和环境变量

在 Linux 中,进程在内存中一般会分为五个段,包含了从磁盘载入的程序代码以及其他数据。即代码段、数据段、BSS段、堆、栈

  • 0xC0000000 ~ 0xFFFFFFFF
    • 内核空间

在这里插入图片描述

每一个进程的用户空间是私有的,但是内核空间是共享的。例如匿名管道通信,就是在内核空间中分配出一部分内存,进程1往里写内容,进程2和3都能看见。

二、函数的调用堆栈详细过程

int sum(int a, int b)
{
	int temp = 0;
	temp = a + b;
	return temp; 
}

int main()
{
	int a = 10;
	int b = 20;

	int ret = sum(a, b);
	cout << "ret:" << ret <<endl;
	return 0;
}

问题一:main函数调用sum,sum执行完后,怎么知道回到哪个函数
问题二:sum函数执行完,回到main函数后,怎么知道从哪一行指令继续运行

在这里插入图片描述
程序分析:
int a = 10; → \to mov dword ptr[ebp-04H], 0AH
int b = 20; → \to mov dword ptr[ebp-08H], 14H
int ret = sum(a, b);编译后会将位置为ptr[ebp-0Ch]命名为ret,之后是调用函数,先从右向左向栈顶压入形式参数a和b,同时esp也会随之移到栈顶,即

mov eax, dword ptr[ebp-08H]
push eax
mov eax, dword ptr[ebp-04H]
push eax
call sum  // 函数调用指令,会做两件事,将下一条命令的地址(0x08124458)压栈,进入sum
 // sum函数返回后
add esp, 8   // 本条指令地址(假如地址为0x08124458)将给形参分配的地址交还给系统
mov dword ptr[ebp-0CH], eax   // 将结果放到ret中

由此也可见,在函数调用过程中,形参的内存开辟是在调用函数时就分配好的

进入sum函数,在int temp = 0;执行之前,即左括号{int temp = 0;之间,会执行下面的汇编代码

push ebp  // 此时ebp指向main函数栈帧的栈底,把此地址记录下来
mov ebp, esp  // 把esp赋给ebp,此时ebp指向sum函数栈帧的栈底
sub esp, 4CH  // 给sum函数开辟栈帧空间

int temp = 0; → \to mov dword ptr[ebp-04H], 0
temp = a + b;

mov eax, dword ptr[ebp+0CH]  // 取形参b的值存到eax
add eax, dword ptr[ebp+08H]  // 取形参a的值,和b相加,存到eax
mov dword ptr[ebp-04H], eax  // a+b结果存到temp

return temp; → \to mov eax, dword ptr[ebp-04H]

右括号},回退栈帧

mov esp, ebp  // 把ebp赋给esp,把栈空间归还给系统,但并未清空栈中内容
pop ebp  // 出栈,并把栈里的数值给ebp,即退回main函数栈帧的栈底,同时esp+4
ret  // 出栈,把出栈内容(0x08124458)放在CPU的PC寄存器中,同时esp+4

返回main函数中

 // sum函数返回后
add esp, 8   // 本条指令地址(假如地址为0x08124458)将给形参分配的地址交还给系统
mov dword ptr[ebp-0CH], eax   // 将结果放到ret中

之后再打印,return,结束程序

注:

数值 ≤ 4B,通过eax寄存器带出
4B < 数值 <= 8B,通过eax和edx两个寄存器带出
数值 > 8B,函数调用之前产生临时量,再把临时量地址入栈,被调用函数return处通过偏移ebp访问临时量。

三、程序编译链接原理

编译过程: 预编译 → \to 编译 → \to 汇编 → \to 二进制可重定位的目标文件(*.obj / *.o)

链接过程: 编译完成的所有.o文件 + 静态库文件(Linux下是*.a,Windows下是*.lib)
两个核心步骤:(1)所有.o文件段的合并;符号表合并后,进行符号解析
       (2)符号的重定位(重定向)【链接的核心】

最终在工程目录下 → \to win下得到xxx.exe,Linux下得到a.out

我们需要关注的点:

  1. *.o 文件的格式组成是什么样子的?
  2. 可执行文件的组成格式是什么样子的?
  3. 链接的两步做的是什么事情?
  4. 符号表的输出 → \to 符号,符号怎么理解?
  5. 符号什么时候分配虚拟地址(在用户空间上)?

程序:
main.cpp:

//引用sum.cpp文件里面定义的全局变量以及函数
extern int gdata;
int sum(int, int);

int data = 20;

int main()
{
	int a = gdata;
	int b = data;
	
	int ret = sum(a, b);
	
	return 0;
}

sum.cpp:

int gdata = 10;
int sum(int a, int b)
{
	return a+b;
}

1. 编译过程

C++文件预编译编译汇编二进制可重定位的目标文件(*.obj / *.o)
main.cpp
sum.cpp
处理#开头的命令语法分析、语义分析、词法分析、代码优化
g++ -O 0/1/2/3 指定优化等级
编译完成之后生成特定架构下的汇编代码main.o
sum.o

预编译阶段:#pragma lib 和 #pragma link 例外,不是在预编译阶段完成的,而是在链接阶段完成的,这俩是用于处理链接阶段的外部库文件

现在来看我们的程序

首先进行编译g++ -c xxx.cpp
在这里插入图片描述
符号表:汇编器在把汇编码转成最终的.o文件时就会生成一个符号表

看一下符号表objdump -t xxx.o
在这里插入图片描述

可以看到左边全为0,即编译过程中符号不分配虚拟地址,在链接过程中分配虚拟地址

分析:
在这里插入图片描述

如果引用了外部文件,也会将外部文件中的符号产生在自己的符号表中。如果定义了main函数,则在符号表中函数的符号就是函数名,放在.text(代码段);定义了全局变量data且值为20不等于0,因此放在.data(数据段);引用的gdata也产生了符号gdata,sum也产生了符号_z3sumii,但他们都是*UND*,这是符号的引用,而不是符号的定义。

sum.o文件的符号表中中,需要由函数名字和形参列表一起产生符号,例如这里的sumii解释为sum_int_int

符号表的第二列,l表示locallocal的符号只能在当前文件中看见;g表示globalglobal的符号在其他文件也看得见。因此在链接时,所有.obj文件在一起链接,链接器可以看见所有global的符号,但看不见local符号。

.o文件的组成,可以用readelf -S main.o打印段表,用readelf -h main.o打印文件头(节头部表):

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

回答问题1:*.o 文件的格式组成是什么样子的?
答:由上图可见,是由各种段组成的(elf文件头 .text .data .bss .symtab 等等)

编译完成后,.o文件代码段放入的指令如下,此时符号的地址位置填充的是0,这也是.o文件无法运行的原因之一,可以用objdump -S main.o打印代码段
在这里插入图片描述

2. 链接过程

步骤一:

  • 所有.o文件段的合并:在链接过程中,就要将main.osum.o的各个段进行合并,如.text段和.text段进行合并,.data段和.data段进行合并,.bss段和.bss段进行合并。包括段表和符号表,全部都进行合并。
  • 符号表合并后,进行符号解析:所有对符号的引用,都要找到该符号定义的地方。从原本的*UND*找到对应的在.text.data上的定义。如果链接器没有找到对引用符号的定义,会报错“符号未定义”;如果找到多个对符号的定义(重定义),会报错“符号重定义”在符号解析成功后,给所有的符号分配虚拟地址。

步骤二:

  • 符号的重定位(重定向):将代码段中的对应符号地址修改为为其分配的虚拟地址。

链接器指定入口并进行链接ld -e main *.o,其中-e是指定main作为入口,这样在链接生成的输出文件a.out文件的文件头会将main函数的第一行地址401000作为入口点地址进行记录

objdump -t a.out

在这里插入图片描述

可以看到所有符号都分配地址了,都放到对应的位置了

objdump -S a.out

在这里插入图片描述

readelf -S a.out

在这里插入图片描述

回答问题2:可执行文件的组成格式是什么样子的?
答:由上图可见,可执行文件也是由各种段组成的

readelf -h a.out

在这里插入图片描述

可以看到这是可执行文件,入口是main函数的第一行地址401000

readelf -l a.out

在这里插入图片描述

可执行文件的段和重定向文件的段几乎一致,只是多了一个program headers段,可用readelf -l a.out打印。运行可执行文件的时候,program headers段中LOAD哪些段,就是告诉系统把哪些段加载到内存中,如上图,一般会将.text段和.data段加载到内存中

运行一个可执行文件:

  • 加载哪些内容 → \to 看program headers段
  • 从哪里开始运行 → \to 文件头中的入口地址
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/638973.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【传知代码】Modnet 人像抠图-论文复现

文章目录 概述原理介绍核心逻辑ModNet 的结构 环境配置WebUI 小结 论文地址 论文GitHub 本文涉及的源码可从Modnet 人像抠图该文章下方附件获取 概述 人像抠图技术在多个领域有着广泛的应用场景&#xff0c;包括但不限于&#xff1a; 展馆互动拍照&#xff1a;展馆中使用的抠…

K8S认证|CKA题库+答案| 11. 创建PVC

11、创建PVC 您必须在以下Cluster/Node上完成此考题&#xff1a; Cluster Master node Worker node ok8s master …

玩转OpenHarmony智能家居:如何实现开发版“碰一碰”设备控制

一、简介 “碰一碰”设备控制&#xff0c;依托NFC短距通信协议&#xff0c;通过碰一碰的交互方式&#xff0c;将OpenAtom OpenHarmony&#xff08;简称“OpenHarmony”&#xff09;标准系统设备和全场景设备连接起来&#xff0c;解决了应用与设备之间接续慢、传输难的问题&…

Java web应用性能分析之【高并发之缓存-多级缓存】

说到缓存&#xff0c;作为java开发第一时间想到的是不是上图所示的Redis&#xff0c;又或者是Guava Cache、Caffeine、EhCache这些&#xff1b;Redis作为分布式缓存、其他的可以作为本地缓存。但是作为一名资深开发人员&#xff0c;着眼的层面应该再提升一个级别&#xff0c;从…

LLM多模态——GPT-4o改变人机交互的多模式 AI 模型应用

1. 概述 OpenAI 发布了迄今为止最新、最先进的语言模型 – GPT-4o也称为“全“ 模型。这一革命性的人工智能系统代表了一次巨大的飞跃&#xff0c;其能力模糊了人类和人工智能之间的界限。 GPT-4o 的核心在于其原生的多模式特性&#xff0c;使其能够无缝处理和生成文本、音频…

微信小程序开发环境的搭建

一、注册微信小程序账号 二、安装微信开发者工具 1.下载微信开发者工具。 官网下载地址&#xff1a;https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/downloads.html 2、选择稳定版Window64下载安装 3、下载完毕后&#xff0c;点击下一步安装 三、使用微信开发者工具…

slam14讲(第8讲、前端里程计)LK光流、直接法

直接法的引出 因为第7讲大部分都是讲特征点法&#xff0c;通过提取orb特征点和点的描述子&#xff0c;来构建两帧图像之间的特征点对应关系。这种方法会有缺点&#xff1a; 关键点和描述子提取计算耗时&#xff0c;如果相机的频率高&#xff0c;则slam算法大部分耗时被占。特…

2种方法将集合数据List构建成树形结构

文章目录 递归循环构建树结构hutool.TreeUtil.build构建树结构 递归循环构建树结构 先查最外层树节点数据&#xff0c;再递归遍历每一层子节点数据 public ApiResultDto<List<LocationDto>> getTreeByParams(LocationSearchDto searchDto, SecurityUser user) {// …

代码随想录算法训练营第三十六天|860.柠檬水找零、406.根据身高重建队列、452. 用最少数量的箭引爆气球

860.柠檬水找零 文档讲解&#xff1a;代码随想录 题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 注意看提示&#xff1a; bills[i] 不是 5 就是 10 或是 20 场景较为固定 遇到了20&#xff0c;优先消耗10 class Solution:def lemonadeChange(self, bills: …

秋招突击——算法——模板题——区间DP——合并石子

文章目录 题目内容思路分析实现代码分析与总结 题目内容 思路分析 基本思路&#xff0c;先是遍历区间长度&#xff0c;然后再是遍历左端点&#xff0c;最后是遍历中间的划分点&#xff0c;将阶乘问题变成n三次方的问题 实现代码 // 组合数问题 #include <iostream> #in…

宝塔Linux下安装EMQX服务并设置匿名访问

简述 之前有在Windows和Linux下搭建过EMQX服务并且使用方面都没问题,但那都是使用的用户和密码方式访问,且前提都是通过浏览器进入EMQX的配置页面设置的属性; 但这次使用的是腾讯云租用的宝塔Liniux,由于没有浏览器只能通过命令行方式修改EMQX配置以达到目的;由于事先没看…

使用 CapSolver API 服务解决 Arkose Labs FunCaptcha 验证码

使用 CapSolver API 服务解决 Arkose Labs FunCaptcha 验证码 FunCaptcha 以其复杂的图像验证而闻名&#xff0c;对自动化系统构成了巨大的挑战。CapSolver 的 API 服务利用先进的 AI 技术轻松应对和解决 FunCaptcha 挑战。本指南探讨了 CapSolver 如何实现无缝自动化&#xff…

汇聚荣科技有限公司优点有哪些?

在当今快速发展的科技时代&#xff0c;企业之间的竞争愈发激烈。作为一家专注于科技创新与研发的公司&#xff0c;汇聚荣科技有限公司凭借其卓越的技术实力和创新能力&#xff0c;在业界树立了良好的口碑。那么&#xff0c;汇聚荣科技有限公司究竟有哪些优点呢?接下来&#xf…

XX数字中台技术栈及能力

XX数字中台技术栈及能力 1 概述 XX数字中台面向数据开发者、数据管理者和数据应用者&#xff0c;提供数据汇聚、融合、治理、开发、挖掘、共享、可视化、智能化等能力&#xff0c;实现数据端到端的全生命周期管理&#xff0c;以共筑数字基础底座&#xff0c;共享数据服务能力…

The Missing Semester of Your CS Education(计算机教育中缺失的一课)

Shell 工具和脚本(Shell Tools and Scripting) 一、shell脚本 1.1、变量赋值 在bash中为变量赋值的语法是foobar&#xff0c;访问变量中存储的数值&#xff0c;其语法为 $foo。 需要注意的是&#xff0c;foo bar &#xff08;使用空格隔开&#xff09;是不能正确工作的&…

llama-factory学习个人记录

框架、模型、数据集准备 1.llama-factory部署 # 克隆仓库 git clone https://github.com/hiyouga/LLaMA-Factory.git # 创建虚拟环境 conda create --name llama_factory python3.10 # 激活虚拟环境 conda activate llama_factory # 安装依赖 cd LLaMA-Factory pip install -…

Java 使用继承和重写父类方法写一个商品入库案例

package 练习.商品入库;import java.util.Scanner; // 抽象手机类 public abstract class Phone {//测试方法public static void main(String[] args){// 华为手机huawei h new huawei();h.setName("华为P40");h.setPrice(1999.99);h.setConfiguration("8128GB…

Go知识点复习

Go知识点复习 1.关于包的使用和GOPATH的配置 src:用于以代码包的形式组织并保存Go源码文件, 需要手动创建pkg目录&#xff1a;用于存放经由go install命令构建安装后的代码包&#xff08;包含Go库源码文件&#xff09;的“.a”归档文件bin目录&#xff1a;与pkg目录类似&…

kind: Telemetry

访问日志 访问日志提供了一种从单个工作负载实例的角度监控和理解行为的方法。 Istio 能够以一组可配置的格式为服务流量生成访问日志&#xff0c; 使操作员可以完全控制日志记录的方式、内容、时间和地点。 有关更多信息&#xff0c;请参阅获取 Envoy 的访问日志。 https:/…

ThingsBoard如何拆分前后端分离启动

后端启动 前端启动 注意事项 ThingsBoard是一个开源的物联网平台&#xff0c;它原本的设计就考虑到了现代Web应用的前后端分离架构。尽管其核心是一个后端服务&#xff0c;负责设备连接、数据处理和存储等&#xff0c;但其用户界面是作为单独的前端应用程序实现的&#xff0c…