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…

深入解析生成对抗网络(GAN)

1. 引言 背景介绍 在过去的几十年中,深度学习在计算机视觉、自然语言处理和语音识别等领域取得了巨大的突破。然而,如何让机器生成高质量、逼真的数据一直是人工智能领域的挑战。传统的生成模型,如变分自编码器(VAE)…

无人机动力系统测试-实测数据与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)是一个满足以下条件的矩阵: 非负矩阵:矩阵中的每个元素都是非负的…

教资考试题目

综合多选题 高等教育政策在评估的实施阶段需要完成的工作有(BCD) A. 制定评估计划 B. 收集整理政策信息 C. 统计、分析政策信息 D. 充分运用评估方法获取结论 恪尽师者规范,严守师德“红线”,需要教师(ABCD&…

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 年第十届数维杯国际数模竞赛赛题浅析

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

Spring Boot 集成 Kettle

Kettle 简介 Kettle 最初由 Matt Casters 开发,是 Pentaho 数据集成平台的一部分。它提供了一个用户友好的界面和丰富的功能集,使用户能够轻松地设计、执行和监控 ETL 任务。Kettle 通过其强大的功能和灵活性,帮助企业高效地处理大规模数据集…

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

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

【Linux】内核模版加载modprobe | lsmod

modprobe modprobe 是一个用于加载和卸载 Linux 内核模块的命令。它不仅能够加载单个模块,还能处理模块之间的依赖关系,确保所有依赖的模块都被正确加载。以下是一些关于 modprobe 命令的基本用法和常见选项的详细介绍。 基本语法 modprobe [option…

matlab 读取csv

需要跳过第一行表头等信息 1、读取整个文件 csvread(FILENAME)%文件路径 文件名2、指定起始位置 csvread(FILENAME, R, C)%从文件的第R行和第C列开始读取数据 逗号分开3、指定数据范围 csvread(FILENAME, R, C, [R1 C1 R2 C2])%读取从(R1, C1)到(R2, C2)范围内的数据注意&am…

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

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

记录一下跨域的问题,讲讲跨域

一、为什么有跨域 跨域问题本质上是由于浏览器的同源策略(Same Origin Policy)引起的。这个策略是为了增强网页的安全性,防止恶意网站获取用户的敏感信息。也就是说经过浏览器的才有跨域,在前端代码中进行数据请求的时候往往都要…

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

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

hbase集成phoenix

1.环境 环境准备 三台节点zookeeper三节点hadoop三节点hbase三节点 2.pheonix集成 官网下载地址,需挂梯子,使用官网推荐的对应hbase版本即可 https://phoenix.apache.org/download.html下载及解压 wget https://dlcdn.apache.org/phoenix/phoenix-…

YOLOv5、YOLOv6、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLOv11 推理的 C++ 和 Python 实现

yolo 推理 YOLOv5、YOLOv6、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLOv11 推理的 C 和 Python实现。 支持的推理后端包括 Libtorch/PyTorch、ONNXRuntime、OpenCV、OpenVINO 和 TensorRT。 支持的任务类型包括分类、检测和分割。 支持的模型类型包括FP32、FP16和INT8。 yolo测…

借助 Pause 容器调试 Pod

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