g++演示如何从C++代码到可执行程序




🎬 个人主页:Vect个人主页

🎬 GitHub:Vect的代码仓库

🔥 个人专栏: 《数据结构与算法》《C++学习之旅》《Linux》
⛺️Per aspera ad astra.


文章目录

  • 1. C++如何从代码到可执行文件
    • 1.0. 一小段代码进行演示
    • 1.1. 预处理阶段: `g++ -E`
    • 1.2.编译阶段:`g++ -S`
    • 1.3. 汇编阶段:`g++ -c`
    • 1.4. 链接阶段:`g++ main.o foo.o -o app`
    • 1.5. 把模板定义放到`.cpp`会发生什么?
  • 2. 动态库和静态库
    • 2.1. 动态库
    • 2.2. 静态库
    • 2.3. 二者对比
  • 3. 总结

1. C++如何从代码到可执行文件

1.0. 一小段代码进行演示

// add.h#pragmaoncetemplate<classT>Tadd(T a,T b){returna+b;}
// foo.cpp#include"add.h"intfoo(){returnadd(10,30);// add<int>实例化}
// main.cpp#include"add.h"#include<iostream>usingnamespacestd;intfoo();intmain(){cout<<add(1,2)<<endl;cout<<foo()<<endl;return0;}

1.1. 预处理阶段:g++ -E

预处理阶段编译器只做文本级工作:

  • 展开#include:把头文件的内容拷贝进来
  • 宏替换#define
  • 条件编译#if/#ifdef
  • 去掉注释

注意:模板实例化不在预处理阶段,预处理器不懂C++语义,只做文本级处理

命令演示

[vect@VM-0-11-centos ~]$ g++ -E main.cpp -o main.i[vect@VM-0-11-centos ~]$ g++ -E foo.cpp -o foo.i

-E:只执行预处理操作,预处理结束就停止

-o:指定输出文件名,后面紧跟文件名

filename.i:预处理后的源文件后缀为.i

如果不加-o main.i预处理过后的文件会输出到终端

1.2.编译阶段:g++ -S

把预处理后的.i文件变成汇编文件.s

  • 词法/语法分析:把字符流变成token流(我们写的代码对于机器来说就是一串字符,编译器会把这串字符组合成有意义的”单词“即token流);判断token流的排列是否符合C++语法并构建AST树(抽象语法树)

    • 写的一行代码:int a = b + 3;在计算机眼里就是一串字符:i n t a = b + 3 ;,现在把这些字符组成有意义的token,具体包括了关键字、标识符、运算符、字面量、语句结束符,这个阶段只关心”单词的构建“,不关系语法是否正确

    • 现在已经形成token流[int] [a] [=] [b] [+] [3] [;],语法分析的结果不是对与错,而是构建AST树(这句代码真正的结构含义)

    = / \ a + / \ b 3

    然后补充上类型信息:

    声明语句 ├──类型:int └── 赋值 ├──变量:a └── 加法 ├──变量:b └──常量:3

    这里可以类比:词法分析-认识单词 语法分析-分析句子主谓宾 AST-句子的语法树状图

  • 语义分析:类型检查、重载检查、名字查找、访问控制、模板相关规则

  • 模板实例化:当编译器看到”需要用到的模板“时,会生成具体版本的函数体,例如:add(1,2)--->add<int>(int,int)

  • 优化:基于AST树,修改AST树,常量折叠(直接进行运算,不留到运行期int x = 5 + 3 -> int x = 8)、内联(直接替换函数调用,在这里展开函数,内联只是建议)、死代码删除(删除永远不会执行的代码)、寄存器分配…

  • 生成汇编:输出.s

命令演示:

[vect@VM-0-11-centos ~]$ g++ -S main.i -o main.s[vect@VM-0-11-centos ~]$ g++ -S foo.i -o foo.s

可以观察到,此时已经形成了汇编代码

1.3. 汇编阶段:g++ -c

汇编把.s汇编代码变成机器码目标文件(二进制文件).o,二进制文件包含:

  • .text段:机器指令

  • .rodata:只读常量

  • .data/.bss:全局/静态数据

  • 符号表:目标文件中定义了哪些符号、还需要外部提供哪些符号

    符号:函数名、全局变量名、静态变量名

    符号表:**目标文件里的一张”名字->状态/地址“**的表,回答定义了哪些符号,使用了哪些符号但是还不知道地址,举个例子理解一下:

    // show_signal.cppintfoo(){return23;}

    编译:

    [vect@VM-0-11-centos ~]$ g++ -c show_signal.cpp -o foo.o[vect@VM-0-11-centos ~]$ nm -C foo.o# 查看符号表U __cxa_atexit U __dso_handle 0000000000000048 t _GLOBAL__sub_I__Z3foov 0000000000000000 T foo()000000000000000b t __static_initialization_and_destruction_0(int, int)U std::ios_base::Init::Init()U std::ios_base::Init::~Init()0000000000000000 b std::__ioinit

    0000000000000000 T foo()偏移地址 在.text段中定义 符号名

    说明了在foo.o里面,自己定义了foo

    再看需要外部提供符号的情况:

    intfoo();// 声明未定义intmain(){returnfoo();}

    编译:

    [vect@VM-0-11-centos ~]$ g++ -c main_signal.cpp -o main.o[vect@VM-0-11-centos ~]$ nm -C main.o U __cxa_atexit U __dso_handle 0000000000000048 t _GLOBAL__sub_I_main 0000000000000000 T main U foo()000000000000000b t __static_initialization_and_destruction_0(int, int)U std::ios_base::Init::Init()U std::ios_base::Init::~Init()0000000000000000 b std::__ioinit

    看这段代码:

    0000000000000000 T main U foo()

    U = undefined未定义,这里说明在mian.o中用到了foo(),但是在mian.o中没有实现,需要外部提供的符号!

    所以符号表的作用:当链接器拿到main.o发现需要foo,而foo.o定义了foo,则指向foo.o里的foo地址

  • 重定位信息:哪些地址等链接时再决定

    1. 一个残酷的事实:在.o文件中,所有地址都是临时的!!!因为.o不知道将来和谁链接,不知道程序从内存哪里开始,所以编译器只能做到:将来这里要用一个地址,我先占个坑

    2. 举个例子:

    intfoo();// 声明未定义intmain(){returnfoo();}

    在汇编层面:call _Z3foov这里foo被改名为_Z3foov,这里可以补充一个知识点:为什么C++支持函数重载?在汇编和链接层面,名字必须唯一,C++函数会进行函数名改编,把函数名编进符号表里

    _Z 3 foo v │ │ │ │ │ │ │ └── 参数列表:v = void(无参数) │ │ └─────── 函数名 foo │ └─────────── 3 表示 foo 这个名字长度是 3 └─────────────── _Z = C++ 符号前缀
    voidfoo();// _Z3foovvoidfoo(int);// _Z3fooivoidfoo(double);// _Z3food

    常见的类型编码:

    类型编码
    voidv
    inti
    doubled
    charc
    longl
    指针P

    这里我们也可以用nm main_signal.o来查看

    1. 编译器如何解决这个问题?

    生成机器码+留一个备注(重定位信息)

    1. 重定位表是啥:一张需要补地址的清单

总结一下,在汇编阶段编译器的行为:收集所有符号表->给所有符号分配最终地址->处理重定位的信息而符号表表达了谁是谁,重定位信息表达了地址填哪

1.4. 链接阶段:g++ main.o foo.o -o app

编译器把多个.o文件(包括库文件.a/.so)拼成一个可执行文件或共享库:

  • 符号解析:main.o未定义的符号去别的.o/.a/.so里找定义
  • 地址分配和段合并:把各个.text/.data合并,给每个符号分配最终地址
  • 重定位:把机器码/数据中”占位“的地址改成最终地址
  • 处理库依赖:
    • 静态库.a:把需要的目标文件成员抽取进行最终程序
    • 动态库.so:记录依赖关系,运行时由动态装载器加载

命令演示

[vect@VM-0-11-centos link]$ g++ main.o foo.o -o app[vect@VM-0-11-centos link]$ ./app[vect@VM-0-11-centos link]$ ldd ./app# 查看依赖的动态库linux-vdso.so.1=>(0x00007ffd751fe000)libstdc++.so.6=>/home/vect/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6(0x00007ff731b8b000)libm.so.6=>/lib64/libm.so.6(0x00007ff731889000)libgcc_s.so.1=>/lib64/libgcc_s.so.1(0x00007ff731673000)libc.so.6=>/lib64/libc.so.6(0x00007ff7312a5000)/lib64/ld-linux-x86-64.so.2(0x00007ff731f0c000)

1.5. 把模板定义放到.cpp会发生什么?

add.h只写声明

// add.h#pragmaoncetemplate<typenameT>Tadd(T a,T b);// 只有声明,没有定义

add.cpp定义模板

// add.cpp#include"add.h"template<typenameT>Tadd(T a,T b){returna+b;}
// main.cpp#include<iostream>#include"add.h"intmain(){std::cout<<add(1,2)<<std::endl;return0;}

编译每个cpp都成功了:

[vect@VM-0-11-centos template]$ g++ -c add.cpp -o add.o[vect@VM-0-11-centos template]$ g++ -c main.cpp -o main.o

这里已经埋雷了

链接出错:

[vect@VM-0-11-centos template]$ g++ add.o main.o -o app main.o: Infunction`main': main.cpp:(.text+0xf): undefined reference to`int add<int>(int, int)' collect2: error: ld returned1exitstatus

为什么会出错?

  1. 编译main.cpp发生了什么?

    add(1,2);

    • 编译器知道这是个模板

    • 需要生成add<int>函数体

    • 但在add.h里只看到声明,没有定义

      于是编译器只能假设将来有人实现add<int>

      mian.o里:符号表是U int add<int>(int, int)

  2. 编译add.cpp发生了什么?

    [vect@VM-0-11-centos template]$ nm -C add.o[vect@VM-0-11-centos template]$

    符号表是空的!!!

    add.cpp没有任何地方用到add<int>,编译器遵循模板哪里使用哪里实例化的原则

  3. 链接发生了什么?

    文件情况
    main.o我需要add<int>
    add.o我没定义add<int>

    现在没人提供这个符号!!!

    报错:undefined reference to int add<int>(int, int)

本质说明了:模板的实例化发生在编译期,而不是链接期

所以,怎么解决?

  • 模板定义放在头文件
  • add.cpp文件中显式实例化

2. 动态库和静态库

动态库:用的时候,程序只记住去哪里找,真正运行时再加载

静态库:用的时候,把代码直接拷贝到程序里

我们还是用add这份代码,不要模板

2.1. 动态库

Linux下:.so为后缀的文件,本质是独立存在的二进制文件

怎么生成?

  • 生成位置无关代码:g++ -fPIC add.cpp -o add.o-fPIC告诉编译器这段代码将来被共享

  • 生成动态库:g++ -shared add.o -o libadd.so生成了动态库:libadd.so

  • 动态库链接:g++ main.cpp -L. -ladd -o app_dynamic

    -L.:在当前目录找库

    -ladd:优先找libadd.so

  • 验证依赖:ldd app_dynamic

完整代码:

[vect@VM-0-11-centos rep]$ g++ -fPIC -c add.cpp -o add.o[vect@VM-0-11-centos rep]$ g++ -shared add.o -o libadd.so[vect@VM-0-11-centos rep]$ g++ main.cpp -L. -ladd -o app_dynamic[vect@VM-0-11-centos rep]$ g++ main.cpp -L. -ladd -o app_dynamic[vect@VM-0-11-centos rep]$ ./app_dynamic3[vect@VM-0-11-centos rep]$ ldd app_dynamic linux-vdso.so.1=>(0x00007ffe6c5bd000)libadd.so(0x00007fdfcef90000)libstdc++.so.6=>/home/vect/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6(0x00007fdfcec0f000)libm.so.6=>/lib64/libm.so.6(0x00007fdfce90d000)libgcc_s.so.1=>/lib64/libgcc_s.so.1(0x00007fdfce6f7000)libc.so.6=>/lib64/libc.so.6(0x00007fdfce329000)/lib64/ld-linux-x86-64.so.2(0x00007fdfcf192000)

动态库:编译时只记住地址,运行时再加载代码

2.2. 静态库

Linux下:.a为后缀的文件,本质是一堆.o文件的打包

怎么生成?

  • 编译成目标文件:g++ -c add.cpp -o add.o

    此时add.o里面有add的机器码,还没有生成程序

  • 打包成静态库:ar rcs libadd.a add.o

    现在有静态库:libadd.a

  • 链接生成可执行程序:g++ main.cpp libadd.a -o app_static

    main.cpp用了addlibadd.a里刚好有add.o直接把add.o复制进最终程序

  • 查看依赖:ldd app_static

    会发现没有libadd.a,其实代码已经拷贝到程序里了

完整代码:

[vect@VM-0-11-centos rep]$ g++ -c add.cpp -o add.o[vect@VM-0-11-centos rep]$ ar rcs libadd.a add.o[vect@VM-0-11-centos rep]$ g++ main.cpp libadd.a -o app_static[vect@VM-0-11-centos rep]$ ./app_static3[vect@VM-0-11-centos rep]$ ldd app_static linux-vdso.so.1=>(0x00007ffd18faa000)libstdc++.so.6=>/home/vect/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6(0x00007f51a6245000)libm.so.6=>/lib64/libm.so.6(0x00007f51a5f43000)libgcc_s.so.1=>/lib64/libgcc_s.so.1(0x00007f51a5d2d000)libc.so.6=>/lib64/libc.so.6(0x00007f51a595f000)/lib64/ld-linux-x86-64.so.2(0x00007f51a65c6000)

静态库:编译时把代码拷贝到而可执行文件

2.3. 二者对比

对比点静态库动态库
add 的代码拷进 app在 libadd.so
可执行文件
ldd看不到 add能看到 libadd.so
运行依赖必须有 so

3. 总结

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

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

相关文章

详细介绍:Spring Boot 整合 Thymeleaf(视图层)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

电脑音频录制工具(语音聊天录音软件)

日常工作与生活中,线上会议存档、课程音频记录、音乐片段收藏等音频留存需求十分常见。本文分享一款实用的电脑音频录制工具,其支持全品类软件兼容(涵盖聊天工具、会议平台、游戏客户端、音视频播放器等),可灵活实…

【模板】静态区间最值【牛客tracker 每日一题】

【模板】静态区间最值 时间限制&#xff1a;5秒 空间限制&#xff1a;1024M 网页链接 牛客tracker 牛客tracker & 每日一题&#xff0c;完成每日打卡&#xff0c;即可获得牛币。获得相应数量的牛币&#xff0c;能在【牛币兑换中心】&#xff0c;换取相应奖品&#xff0…

Ascend C 与 CUDA 的对比分析-为异构计算开发者提供迁移指南

目录 &#x1f3af; 摘要 1. 架构哲学&#xff1a;两种不同的AI计算世界观 1.1 &#x1f504; 从"通用加速"到"AI原生"的范式转移 1.2 &#x1f3d7;️ 硬件架构的本质差异 2. 编程模型对比&#xff1a;从线程到任务块的范式革命 2.1 ⚙️ CUDA的线程…

CF1004D Sonya and Matrix - crazy-

思维,构造 题解 已知一个无限大的矩阵,其中每个格子的值表示其距离矩阵中心的曼哈顿距离。 如图是矩阵的一部分:现在另一个矩阵,已知矩阵中全部的 \(t\) 个数,试在无限大的矩阵中截取出 \(n\times m =t\) 大小的矩…

Markdown编辑完全指南

一、Markdown基础知识 1.1 什么是Markdown Markdown是一种轻量级标记语言&#xff0c;使用纯文本格式编写文档。【优点】&#xff1a; ✓ 纯文本&#xff0c;兼容性好 ✓ 语法简单&#xff0c;易学易用 ✓ 专注内容&#xff0c;不被格式干扰 ✓ 版本控制友好&#xff08;Git等…

DAY37 早停策略和模型权重的保存

浙大疏锦行 import torch import torch.nn as nn import torch.optim as optim from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split import numpy as npirisload_iris() Xiris.data yiris.targetX_train,X_test,y_train,y_testtra…

西门子1200 PLC自由口通讯CRC校验程序实战

西门子1200plc自由口通讯CRC校验程序。 该CRC校验程序校验出来的校验码为modbus RTU协议报文最后的校验码。用于没有modbus RTU指令且没有CRC校验指令的plc&#xff0c;只能用自由口指令来实现modbus RTU通讯协议的情况。 该程序已经实测&#xff0c;功能包好。在自动化控制领域…

【求解释】智子递归架构:基于互补递归与河洛调控的智能系统框架

智子递归架构&#xff1a;基于互补递归与河洛调控的智能系统框架——简化符号、清晰表述与数学实现研究者&#xff1a;桑干河报告版本&#xff1a;1.0发布日期&#xff1a;2023年10月---摘要智子递归架构是一种基于二值递归单元&#xff08;智子&#xff09;、互补对称性与全局…

Node.js `import.meta` 深入全面讲解

import.meta 是 ES 模块&#xff08;ESM&#xff09;特有的元数据对象&#xff0c;提供当前模块的上下文信息&#xff0c;是 ES 标准的一部分&#xff08;ES2020 引入&#xff09;&#xff0c;Node.js 从 v12.2.0 开始支持&#xff08;需启用 ESM&#xff0c;v14.13.0 及以上无…

教程8:结构体的添加和使用-–-behaviac

原文 本文档描述的是3.6及以后版本&#xff0c;对于3.5及以前的老版本请参考分类“3.5”。对于结构体类型的使用&#xff0c;包括新增全新的结构体和扩展使用已有的结构体。对于新增的结构体&#xff0c;又包括编辑器是否自动生成该结构体的代码。 实际上&#xff0c;我们将新…

影刀RPA发货大杀器!亚马逊订单批量发货效率提升2000%,告别手动煎熬![特殊字符]

影刀RPA发货大杀器&#xff01;亚马逊订单批量发货效率提升2000%&#xff0c;告别手动煎熬&#xff01;&#x1f680;每天处理几百个亚马逊订单发货到手软&#xff1f;复制粘贴物流单号到怀疑人生&#xff1f;别硬扛了&#xff01;今天我用影刀RPA打造智能发货机器人&#xff0…

CF1009F Dominant Indices - crazy-

dsu-on-tree,双端队列 题意 给定一棵有 \(n\) 个顶点的有根树,以顶点 \(1\) 作为根。 定义顶点 \(x\) 的深度数组为一个无限序列 \([d_{x, 0}, d_{x, 1}, d_{x, 2}, \dots]\),其中 \(d_{x, i}\) 表示满足以下两个条件…

蓄电池与超级电容器混合储能并网的Simulink仿真探索

蓄电池与超级电容器混合储能并网 Matlab/Simulink仿真模型&#xff0c;波形正确。 1.混合储能并网&#xff0c;能量管理&#xff0c;蓄电池与超级电容混合储能并网&#xff0c;混合储能采用低通滤波器进行功率分配&#xff0c;有效抑制系统功率波动&#xff0c;实现母线电压稳定…

macOS 的两款好用的免费截图软件: shottr 和 snipaste

snipaste https://www.snipaste.com/ shottr https://shottr.cc/ 也有免费版本 好友版本更多更丰富的功能。

教程9:枚举的添加和使用-–-behaviac

原文 本文档描述的是3.6及以后版本&#xff0c;对于3.5及以前的老版本请参考分类“3.5”。对于枚举类型的使用&#xff0c;包括新增全新的枚举和扩展使用已有的枚举。对于新增的枚举&#xff0c;又包括编辑器是否自动生成该枚举的代码。 实际上&#xff0c;我们将新增枚举&am…

QSharedMemory 变量在对象析构的时候要怎么处理

在Qt中使用QSharedMemory时&#xff0c;对象析构时的正确处理至关重要&#xff0c;主要涉及共享内存的释放和跨进程同步问题。以下是关键处理原则和步骤&#xff1a; 1. 自动析构行为&#xff08;Qt内置机制&#xff09; 自动detach()&#xff1a;当QSharedMemory对象销毁时&…

TikTok达人合作订单太繁琐?影刀RPA一键智能处理,效率飙升10倍![特殊字符]

TikTok达人合作订单太繁琐&#xff1f;影刀RPA一键智能处理&#xff0c;效率飙升10倍&#xff01;&#x1f680;作为影刀RPA的资深布道者&#xff0c;我深知MCN机构在达人订单管理上的"手忙脚乱"。今天&#xff0c;就带你用RPA技术打造达人合作"智能中台"&…

投机推理原理及设计

投机推理的核心思想是所谓的“草稿-验证”范式:快速生成多个草稿token,然后利用大语言模型的并行计算性一次验证所有草稿token,并决定接受多少token。例如,我们利用一个只有1B的小模型快速生成了8个草稿token draf…

前端保存用户登录信息 深入全面讲解

前端保存用户登录信息的核心目标是持久化登录状态&#xff08;减少重复登录&#xff09;、提升用户体验&#xff0c;同时必须兼顾安全性&#xff08;防止信息泄露、伪造、劫持&#xff09;。本文从存储方案选型、安全防护、最佳实践、常见问题等维度&#xff0c;全面解析前端登…