【施磊】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 文件头中的入口地址
    在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python将程序运行结果存入txt文本

//其实就是运行下面代码&#xff0c;然后下面代码会通过subprocess再去运行script.py&#xff08;我们的程序代码&#xff09;&#xff0c;然后把它写入oput.txt中。 import subprocess with open(oput.txt, w) as f:subprocess.run([python, script.py], stdoutf, stderrsu…

XX数字中台技术栈及能力

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

酷开系统 | 酷开科技把握智慧先机 AI赋能家庭场景

智慧化是当今世界科技发展的前沿领域之一。现在的智慧化&#xff0c;也正在逐步成为我们日常生活的一部分。电视系统也进入了数字化时代&#xff0c;AI的应用正在不断扩展&#xff0c;其潜力似乎无穷无尽。 酷开科技深耕人工智能技术&#xff0c;在提升语音体验、强化智能家居…

目前流行的前端框架有哪些?

目前流行的前端框架有很多&#xff0c;它们可以帮助开发者快速构建高质量的前端应用程序。本文将介绍一些目前比较受欢迎的前端框架&#xff0c;并分析它们的优缺点。 React React 是一个由 Facebook 开发的开源前端JavaScript库&#xff0c;用于构建用户界面&#xff0c;尤其…

在vue中实现下载文件功能

实际操作为&#xff0c;在表格中 我们可以获取到文件的id&#xff0c;通过插槽就可以实现 <template #default"scope"><el-button type"text" click"handleDown(scope.row)"><span>下载</span></el-button> </…

计算机毕业设计 | springboot+vue汽车修理管理系统 汽修厂系统(附源码)

1&#xff0c;项目背景 在如今这个信息时代&#xff0c;“汽车维修管理系统” 这种维修方式已经为越来越多的人所接受。在这种背景之下&#xff0c;一个安全稳定并且强大的网络预约平台不可或缺&#xff0c;在这种成熟的市场需求的推动下&#xff0c;在先进的信息技术的支持下…

网络协议——Modbus-TCP

目录 1、简介 2、Modbus-TCP与Modbus-RTU的区别 3、消息格式 4、功能码01H 5、功能码02H 6、功能码03H 7、功能码04H 8、功能码05H 9、功能码06H 10、功能码0FH 11、功能码10H 1、简介 Modbus-TCP&#xff08;Modbus Transmission Control Protocol&#xff09;是一…

嵌入式学习——3——TCP-UDP 数据交互,握手,挥手

1、更新源 cd /etc/apt/ sudo cp sources.list sources.list.save 将原镜像备份 sudo vim sources.list 将原镜像修改成阿里源/清华源&#xff0c;如所述 阿里源 deb http://mirrors.aliyun.com/ubuntu/ bionic main …

【qt】QListWidget 组件

QListWidget 组件 一.QListWidget的用途二.界面设计三.QListWidget的添加1.界面添加2.代码添加 四.列表项的设置1.文本2.图标3.复选框4.列表大小 五.字体和图标的设置1.字体&#xff1a;2.图标&#xff1a; 六.设置显示模式1.图标2.列表 七.其他功能实现1.删除2.全选3.反选4.ad…

小微企业管理系统如何选择等保服务?

小微企业在选择等保&#xff08;信息安全等级保护&#xff09;服务时&#xff0c;应当考虑以下几个关键点以确保既能符合法规要求&#xff0c;又能在成本效益上做出合理决策&#xff1a; 了解等保需求&#xff1a;首先&#xff0c;小微企业需要了解自身的业务性质和信息系统的重…

30.包名的修改和新建后端模块

权限和第三方登录确实令人头疼,我们来学一点简单一点的。 另外,如果各位有属于自己的域名和ICP/IP备案,布置一个作业,自行实现第三方QQ登录。 我们所说的包名修改,是一次性修改ruoyi的全部包名,因为发现很多人有这样的需求,下载别人的代码,想要改成自己公司的包名,结…

深入Django项目实战与最佳实践

title: 深入Django项目实战与最佳实践 date: 2024/5/19 21:41:38 updated: 2024/5/19 21:41:38 categories: 后端开发 tags: Django 基础项目实战最佳实践数据库配置静态文件部署高级特性 第一章&#xff1a;Django项目架构与设计原则 Django框架概述 Django是一个高级的P…

上位机图像处理和嵌入式模块部署(mcu的按键输入)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 做技术的同学&#xff0c;大部分都会把精力放在技术本身&#xff0c;却忽视了学的东西有什么实际的用途。就拿gpio来说&#xff0c;一般我们点灯也…

递归的例子

例1&#xff1a;阶乘函数 #include<iostream> using namespace std; int f(int n) {if(n0)return 1;elsereturn f(n-1)*n; } int main() {int n;cin>>n;cout<<f(n);return 0; }例2&#xff1a;Fibonacci数列 无穷数列1&#xff0c;1&#xff0c;2&#xff0…

计算机-编程相关

在 Linux 中、一切都是文件、硬件设备是文件、管道是文件、网络套接字也是文件。 for https://juejin.cn/post/6844904103437582344 fork 进程的一些问题 fork 函数比较特殊、一次调用会返回两次。在父进程和子进程都会返回。 每个进程在内核中都是一个 taskstruct 结构、for…

Python函数进阶:四大高阶函数、匿名函数、枚举、拉链与递归详解

系列文章目录 Python数据类型&#xff1a;编程新手的必修课深入探索Python字符串&#xff1a;技巧、方法与实战Python 函数基础详解Python正则表达式详解&#xff1a;掌握文本匹配的魔法Python文件操作宝典&#xff1a;一步步教你玩转文件读写Python面向对象基础与魔法方法详解…

实验五:实现循环双链表各种基本运算的算法

实验五&#xff1a;实现循环双链表各种基本运算的算法 一、实验目的与要求 目的:领会循环双链表存储结构和掌握循环双链表中各种基本运算算法设计。 内容:编写一个程序cdinklist.cpp,实现循环双链表的各种基本运算和整体建表算法(假设循环双链表的元素类型ElemType为char),并…

【加密与解密(第四版)】第十七章笔记

第十七章 软件保护技术 17.1 防范算法求逆 17.2 抵御静态分析 反汇编算法&#xff1a;线性扫描&#xff08;无法正确地将代码和数据分开&#xff09;、递归进行 巧妙构造代码和数据&#xff0c;在指令流中插入很多“数据垃圾"&#xff0c;干扰反汇编软件的判断&#xf…

百度手机号登录iApp源码V3

本源码直接对接百度网页版登录接口&#xff0c;无需跳转第三方APP或者网页&#xff0c;可直接进行输入手机号获取验证码的步骤&#xff0c;登录后可获取到用户的UID&#xff0c;名称&#xff0c;BDUSS&#xff0c;PTOKEN&#xff0c;STOKEN&#xff0c;头像直链。 源码下载&…

若依nodejs版本过高问题解决方案

由于nodejs版本过高,可能会导致vue-cli项目运行报错。 目录 方法1:每次启动项目前,输入配置命令 方法2:修改package.js