C/C++运行库

文章目录

  • 入口函数
    • glibc入口函数
    • _start
    • __libc_start_main
    • MSVC入口函数
    • 堆初始化
    • IO初始化
  • glibc C运行库
    • glibc启动文件
    • gcc补充C++全局构造与析构
  • 运行库对于多线程的改进
    • 线程局部存储

入口函数

使用C语言编写的一个hello world程序在用户看来的确非常简单,源代码仅需要短短几行便可实现。但是这个程序在操作系统看来可一点都不复杂,仅从语言层面来看的话,main函数是程序的起点,这没有错。但是从操作系统来看,把main函数当作程序起点是一种误解。执行main函数之前系统已经为我们做了很多初始化工作(以printf库函数可以被使用来说,是因为系统在main函数之前已经为我们做了IO初始化)。main函数返回后系统还需要进行收尾工作,进行进程资源释放。

因此程序真正的起点应该是在调用main函数之前的那些代码,这些代码被称为入口函数入口点

程序的完整生命历程

  1. 进程创建后调用入口函数
  2. 入口函数完成堆栈初始化、IO初始化、全局变量初始化……
  3. 入口函数负责调用main函数执行程序主体
  4. 入口函数等待main返回并做资源释放

Linux环境和Windows环境的入口函数实现各异,但基本思想一致。

glibc入口函数

_start

glibc下的入口函数为_start,其定义由汇编直接给出。_start函数本身并没有做很多的事情,它主要的工作是提取命令行参数的个数和命令行参数+环境变量数组,注意此时的命令行参数和环境变量数组被糅合在一块,它俩分离要在下一步实现。同时它获取了main函数地址和其他2个函数地址(顾名思义这2个函数对于初始化和收尾工作具有重要作用)

在调用_start之前,装载器会把用户的参数和环境变量压入栈中

随后_start会调用一个__libc_start_main函数进行各项初始化工作和main函数调用。

image-20241116163143167

__libc_start_main

__libc_start_main的声明形式如下:

int __libc_start_main(int (*main)(int,char**,char**), //main函数地址int argc, //命令行参数个数char* ubp_av,  //参数+环境数组__typeof (main)int, //init函数地址void (*fini)(),    //finit函数地址void (*rtld_fini)(), //动态链接收尾函数地址void* stack_end); //栈顶

① __lib_start_main的第一步就是要获取环境变量表的首地址,这一步很简单,ubp_ev偏移量为argc+1处即环境变量表的首地址,确定位置后将全局变量__environ指向该位置

关键代码

char** ubp_ev=&ubp_av[1+argc];
__environ=ubp_ev;

image-20241116164205402

②随后__lib_start_main注册收尾函数并调用init函数进行资源初始化

关键代码

__cxa_atexit(rtld_fini,0,0);
__lib_init_first(argc,argv,__environ);
__cxa_atexit(fini,0,0);
(*init)(argc,argv,__environ);

__cxa_atexit的效果等价于atexit,它们都用于注册收尾函数,注册的收尾函数在main返回时会按照注册的顺序逆向调用,有一个全局函数指针数组用来保存注册的收尾函数,在main返回时通过遍历调用该数组中的函数就可以实现收尾工作。

image-20241116164955113

③调用main并接受其退出结果

关键代码

exit(main(argc,argv,__environ));

由于glibc的入口函数编写的不够清晰,书中省略了glibc中关于各项具体初始化的介绍,但是可以确定的是这些初始化一定是有对应的函数来处理的。

MSVC入口函数

MSVC的入口函数是mainCRTStartup,它没有像glibc中的入口函数一样分2步走,而是一步到位。基本步骤如下

  1. 获取系统信息
  2. 堆初始化
  3. IO初始化
  4. 获取命令行参数和环境变量
  5. C标准库设置
  6. 调用main并返回

image-20241116165725235

堆初始化

堆初始化在代码上实现是最简单,直接调用了一个_heap_init系统调用,之所以堆初始化放在第一位的原因在于后续初始化工作大量借助堆。如果堆初始化失败程序就运行不了。

IO初始化

每一个进程都有私有的打开文件表,打开文件表需要在入口函数中进行初始化操作。

基本任务为

  1. 建立进程打开文件表
  2. 决定从父进程继承而来的文件句柄是否保留
  3. 初始化标准输入输出错误

_cinit 对于全局变量和全局对象的初始化和释放需要牵涉到更多关于运行库的知识,下文以glibc运行库为例

glibc C运行库

一个C程序能够运行,它必须依赖C运行库。上文概况的入口函数属于C运行库的一部分。C运行库由入口函数及其所依赖的函数和大量的C标准库函数构成,它的基本功能如下

  1. 启动与退出——入口函数
  2. 标准库函数——printf、scanf……
  3. 封装IO和堆操作
  4. 语言特性和调试信息

C标准库是C运行库的主体,C运行库中的crt1.o、crti.o、crtn.o是三个重要的辅助文件,入口函数及其所以来的函数基本都在与这三个辅助文件息息相关。它们三又称为glibc启动文件

glibc启动文件

crt1.o中定义了_start、__libc_main_start函数;crti.o中定义了.init函数的开头;crtn.o定义了.finit函数的结尾;最终生成的可执行文件中会存在.init段和.finit段,顾名思义这两个段就是用于初始化和收尾的,_init函数就定义在.init段,_finit函数就定义在.finit段。_start中所获取的2个函数就是_init和_finit

crti.o和crtn.o只是init和finit函数的一部分,在链接时链接器会合并各个目标文件的.init和.finit段使之拼凑成一个完整的_init和_finit函数

image-20241116180648490

链接器在链接时会自动添加这三个辅助文件

gcc补充C++全局构造与析构

C++全局对象的构造函数和析构函数不放在.init段和.finit段,但却由_init和_finit负责调用,glibc只是一个C运行库,它不能很好的兼容C++,这项任务就交给了gcc。gcc所配置的crtbegin.o和crtend.o就是专门用来扩展C++全局对象构造和析构2个目标文件。

_init的反汇编image-20241116181336022

全局对象的构造函数(__do_global_ctors_aux)在可执行文件中的.ctor段,其中维护了一个指针数组(__CTOR_LIST),数组中存放的正是main函数之前需要被构造的全局对象析构函数地址。

cpp code eg

//module1.cc
struct A{};
A a;
int main(){return 0;}
//module2.cc
struct B{};
B b;
//module3.cc
struct C{};
C c

gcc编译期间会为每一个编译单元的全局对象生成一个特殊函数,这个特殊函数负责对全局对象的初始化和析构函数绑定。之后编译器为该目标文件生成.ctor段,并构建一个_CTOR_LIST数组用于保存该编译单元内的全局对象构造函数。并且链接阶段由链接器将各个目标文件的.ctor段合并拼凑成一个完整的_CTOR_LIST数组。.crtbeginT.o就是.ctor段的开头,而==.crtend就是.ctor段的末尾==

image-20241116183812503

对于全局对象的析构函数gcc并没有提供类似于.ctor段的.dtor段,而是巧妙地利用了atexit函数进行绑定,atexit所绑定的函数在main返回时会按注册顺序逆向调用,这正好于析构函数应该调用的顺寻相吻合。__tcf_1、__tcf_2、_tcf_3就是在调用全局对象的析构函数image-20241116184258569

运行库对于多线程的改进

C/C++标准库并不考虑多线程环境,多线程环境的支持主要有运行库额外提供,例如glibc提供的pthread线程库。C/C++标准库中的malloc和printf都是线程不安全的函数,线程库通过加锁实现了malloc和printf的线程安全,使得我们现在在编写多线程C/C++时不需要显示对malloc和printf之类的函数加锁操作。此外线程库还引入了线程局部存储用于定义线程私有的数据。

线程局部存储

所谓线程局部存储即定义在主线程作用域范围内,但每个线程都有该变量副本的一种机制。在gcc下通过关键字__thread声明,msvc下通过__declspec(thread)声明

gcc:

__thread int number;

msvc:

__deslspec(thread) int number;

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

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

相关文章

学习使用LVGL,依赖官方网址

LVGL Basics — LVGL documentation LVGL基础知识 LVGL是一个开源的图形库,提供创建嵌入式GUI的一切 LVGL数据流 您为每个物理显示面板 创建一个显示器 (lv_display) ,在其上创建屏幕小部件,将小部件添加到这些屏幕上。要处理触摸、鼠标、…

计算机网络HTTP——针对实习面试

目录 计算机网络HTTP什么是HTTP?HTTP和HTTPS有什么区别?分别说明HTTP/1.0、HTTP/2.0、HTTP/3.0请说明访问网页的全过程请说明HTTP常见的状态码Cookie和Session有什么区别?HTTP请求方式有哪些?请解释GET和POST的区别?HT…

大数据-226 离线数仓 - Flume 优化配置 自定义拦截器 拦截原理 了 拦截器实现 Java

点一下关注吧!!!非常感谢!!持续更新!!! Java篇开始了! 目前开始更新 MyBatis,一起深入浅出! 目前已经更新到了: Hadoop&#xff0…

无人机动力系统测试-实测数据与CFD模拟仿真数据关联对比分析

我们经常被问到这样的问题:“我们计划运行 CFD 仿真,我们还需要对电机和螺旋桨进行实验测试吗?我们可能有偏见,但我们的答案始终是肯定的,而且有充分的理由。我们自己执行了大量的 CFD 仿真,但我们承认&…

验证双随机矩阵(doubly stochastic matrix) 满足C(P)=C(P^T)

验证双随机矩阵(doubly stochastic matrix) 满足C( P P P)C(P T ^T T) 双随机矩阵: 在数学中,一个双随机矩阵(doubly stochastic matrix)是一个满足以下条件的矩阵: 非负矩阵:矩阵中的每个元素都是非负的…

Chrome 浏览器开启打印模式

打开开发者工具ctrl shift p输入print 找到 Emulate CSS print media type

Vite初始化Vue3+Typescrpt项目

初始化项目 安装 Vite 首先,确保你的 Node.js 版本 > 12.0.0。然后在命令行中运行以下命令来创建一个 Vite Vue 3 TypeScript 的项目模板: npm init vitelatest进入项目目录 创建完成后,进入项目目录: cd vue3-demo启动…

24 年第十届数维杯国际数模竞赛赛题浅析

本次万众瞩目的数维杯国际大学生数学建模赛题已正式出炉,无论是赛题难度还是认可度,该比赛都是数模届的独一档,含金量极高,可以用于综测加分、保研、简历添彩等各方面。考虑到大家解题实属不易,为了帮助大家取得好成绩…

自动语音识别(ASR)与文本转语音(TTS)技术的应用与发展

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

CentOS8 启动错误,enter emergency mode ,开机直接进入紧急救援模式,报错 Failed to mount /home 解决方法

先看现场问题截图: 1.根据提示 按 ctrld 输入 root 密码,进入系统。 2. 在紧急模式下运行:journalctl -xe ,查看相关日志,找到关键点: Failed to mount /home 3.接着执行修复命令: xfs_repa…

Java项目实战II基于微信小程序的课堂助手(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 在数字化教…

借助 Pause 容器调试 Pod

借助 Pause 容器调试 Pod 在 K8S 中,Pod 是最核心、最基础的资源对象,也是 Kubernetes 中调度最小单元。在介绍 Pause 容器之前需要先说明下 Pod 与容器的关系来理解为什么需要 Pause 容器来帮助调试 1. Pod 与 容器的关系 Pod 是一个抽象的逻辑概念&…

IDEA自定义文件打开格式

介绍在IDEA中自定义文件打开格式的方法,比如一个文件,可以选择用txt格式打开,也可以选择用xml格式打开,也可以用java格式打开等等,通过这个方法可以方便的用任意格式在idea中打开想要打开的文件。 下面分别讨论三种不…

Git 分⽀规范 Git Flow 模型

前言 GitFlow 是一种流行的 Git 分支管理策略,由 Vincent Driessen 在 2010 年提出。它提供了一种结构化的方法来管理项目的开发、发布和维护,特别适合大型和复杂的项目。GitFlow 定义了一套明确的分支模型和工作流程,使得团队成员可以更有效…

ECG心电前级信号提取

由于ECG信号很微弱,处于mV级别,还有很多干扰信号,所以采集信号时需要进行滤波和放大处理,然后使用模数转换。为了滤波高频干扰和工频噪声,需要使用低通滤波器和陷波器抑制噪声,有时也要使用高通滤波器滤除低…

【Android】逆向开发与反逆向开发入门知识(一)

目录 逆向开发反编译 & 反混淆反编译工具反编译反混淆 修改预置资源文件抓包前期准备二次打包重签名 如何预防 App 被逆向开发?代码混淆应用加固防止动态调试Root 检测二次打包检测 警告:逆向开发相关知识请在法律规定范围内使用,请勿使用…

华为Mate 70临近上市:代理IP与抢购攻略

随着科技的飞速发展,智能手机已经成为我们日常生活中不可或缺的一部分。而在众多智能手机品牌中,华为一直以其卓越的技术和创新力引领着行业的发展。近日,华为Mate 70系列手机的发布会正式定档在11月26日,这一消息引发了众多科技爱…

【Linux之权限】理论篇

前言 Linux的权限是我们学习Linux初期非常重要的基础知识,接下来我将通过一个系列【Linux之权限】,共三篇文章,对此进行较为全面和详细的解说。 sudo 情况:如果我们不是超级管理员,但是想执行一个权限级别比较高的指…

[C++] 智能指针

文章目录 智能指针的使用原因及场景分析为什么需要智能指针?异常抛出导致的资源泄漏问题分析 智能指针与RAIIC常用智能指针 使用智能指针优化代码优化后的代码优化点分析 析构函数中的异常问题解决方法 RAII 和智能指针的设计思路详解什么是 RAII?RAII 的…

spark性能优化调优指导性文件

1.让我们看一下前面的核心参数设置: num-executors10||20,executor-cores1||2,executor-memory10||20,driver-memory20,spark.default.parallelism64 假设我们的火花队列资源如下: 内存1T,内…