Linux基本开发工具:编译器gcc/g++

目录

一.认识gcc/g++

二.编译的四个过程

2.1预处理(进行宏替换)

​编辑

2.2编译(生成汇编)

2.3汇编(生成机器可识别的代码)

2.4链接(生成可执行文件和库文件)

2.5.动态链接和静态链接

三.库的理解

3.1动态库

3.2静态库

3.3故事理解动静态库

3.4动静态库的优缺

四.问题归纳

问题一:条件编译与if语句的区别


一.认识gcc/g++

GCC(GNU Compiler Collection,GNU 编译器套件) ,是 Linux 平台最主流的源代码编译器。 简单理解:把 .c / .cpp 文本代码翻译成电脑能执行的程序。

1. 两者区别

  • gcc :主打编译 C 语言 (.c),也可编译 C++,但不会默认链接 C++ 标准库
  • g++ :主打编译 C++ 语言 (.cpp) g++ 本质内部还是调用 gcc,只是自动启用 C++ 相关规则、自动链接 libstdc++

文件后缀自动规则:

  • .c → gcc 默认当作 C 代码
  • .cpp/.hpp → gcc/g++ 当作 C++ 代码

2. 基础编译命令(结合你 vim 写的 test.c)

C 语言示例 test.c

复制代码
gcc test.c -o test
  • test.c:源代码文件
  • -o test:指定输出可执行程序名字为 test

运行程序:

复制代码
./test

如果是 C++ 代码 test.cpp

复制代码
g++ test.cpp -o test

3. 完整编译四大阶段(重点考点)

  1. 预处理 :展开头文件 #include、宏定义 #define
  2. 编译:C/C++ 代码 → 汇编代码
  3. 汇编 :汇编代码 → 机器目标文件 .o
  4. 链接:目标文件 + 系统库 → 最终可执行程序

二.编译的四个过程

预处理 (.c→.i) → 【编译 (.i→.s)】 → 汇编 (.s→.o) → 链接 (.o→可执行程序)

2.1预处理(进行宏替换)

预处理是 gcc/g++ 编译的第 1 步 ,在真正编译代码之前执行,由预处理器 cpp 完成。

所有以 #开头的指令,都是预处理指令。

问:#include 的原理是什么?

答:预处理器将头文件内容原样嵌入到源文件中。

2.2编译(生成汇编)

输入:预处理完成后的文件 .i(纯 C 文本,无#include、无注释、宏全部展开) 输出:汇编代码文件 .s

核心工作:

  1. 词法分析:拆分代码,识别关键字、变量、运算符、字符串;
  2. 语法分析:检查 C 语法是否合法(少分号、括号不匹配这里报错);
  3. 语义分析:检查类型是否匹配(比如 int 赋值给指针、函数参数错误);
  4. 优化:对代码做简单优化;
  5. 生成汇编代码:把合法 C 代码,翻译成对应 CPU 架构的汇编指令。

✅ 关键区分: 编译:C 语言代码 → 汇编代码(人类可读文本 .s) 汇编阶段:汇编代码 → 二进制目标文件(机器码 .o)

问:语法错误在哪个阶段检测?

答:编译阶段。预处理只是文本处理,不会检查语法。

2.3汇编(生成机器可识别的代码)

输入:汇编代码文件 .s(文本,汇编指令)

工具:汇编器 as

输出:目标文件 .o(二进制文件)

工作内容: 将人类可读的汇编指令(push、mov、call)翻译成 CPU 识别的二进制机器码。

⚠️ 重点: .o 是二进制,但无法直接运行! 内部缺少函数地址解析、标准库代码,只是零散的机器码片段。

2.4链接(生成可执行文件和库文件)

输入:一个或多个 .o 目标文件 + 系统库文件

输出:可执行程序(Linux 下没有后缀,例如 test)

两大核心工作

  1. 符号解析 .o 文件里用到的外部函数(比如 printf)只留下名字,没有真实内存地址。 链接器找到每个符号对应的实现,绑定地址。

你的代码调用 printf,printf 的实现不在 test.o 中,在系统 C 标准库 shturl.。

  1. 重定位 调整代码、变量的地址偏移,把零散多个目标文件、库整合到统一地址空间。

2.5.动态链接和静态链接

发生在链接阶段 ,把你的.o目标文件和库结合生成可执行程序。

Linux:静态库.a;动态库.so

三.库的理解

3.1动态库

把一堆函数编译成独立二进制库文件,不把库代码复制到你的程序里面 。 程序运行的时候,才去磁盘加载这个 .so 文件,调用里面的函数。

对比静态库:静态链接会把库代码拷贝复制进可执行程序。

核心特点

  1. 编译链接时,只记录依赖关系,不拷贝库代码 你的可执行程序体积很小。
  2. 运行时才加载库文件 程序启动,操作系统找到 .so,把库加载进内存,程序调用库里面函数。
  3. 共享(共享库名字由来) 多个程序同时使用同一个动态库,内存里只加载一份,节省内存。

前提:你的程序是动态链接生成 的可执行文件。 编译链接阶段仅仅在程序内部写下记录:我需要 libc.so.6、libxxx.so,没有把库代码拷贝进来 。 真正去找库发生在程序运行时刻,由 Linux 的「动态链接器」完成。


是谁负责找库?

ld‑linux‑x86‑64.so.2,这就是系统的动态链接器(加载器) ,它本身也是一个 so。 当你敲下 ./a.out 运行程序时:

  1. 操作系统内核先把你的可执行程序加载进内存
  2. 内核发现:这个程序是动态链接程序,于是启动动态链接器 ld‑linux
  3. ld‑linux 专门负责:寻找、加载所有需要的动态库、解析函数符号地址

📋完整分步流程

第 1 步:读取程序内部的依赖清单

动态链接器打开你的可执行程序,读取里面保存的依赖列表。 比如里面记录:本程序依赖 libc.so.6。

这个清单是编译链接阶段 gcc 写进去的。

第 2 步:按规则搜索磁盘,寻找对应的 .so 文件

ld‑linux 按照固定顺序搜索目录,顺序不能乱:

  1. 程序本身里面写的 rpath(编译时写入的路径,较少用)
  2. 环境变量 LD_LIBRARY_PATH(我们测试经常临时设置这个)
  3. 缓存:读取系统缓存文件 /etc/ld.so.cache(最重要!) 这是一个预生成的二进制缓存,保存系统所有动态库 → 对应文件路径映射,查找速度极快。
  4. 默认系统库目录:/lib、/usr/lib

⚠️注意:默认不会搜索当前工作目录! 这就是为什么你自己写的 libfunc.so 放在当前文件夹,直接./main会报找不到库,必须设置export LD_LIBRARY_PATH=.。

第 3 步:找到 so 文件,把动态库加载进内存

磁盘上找到libxxx.so,把这个 so 文件映射加载到进程虚拟地址空间。

如果多个程序用同一个 so,操作系统在物理内存只放一份,多个进程共享。

第 4 步:符号重定位(填函数地址,最核心)

还记得编译链接阶段,你的.o文件中调用printf,当时不知道 printf 真实地址,只留了一个空坑。

动态链接器现在:

  1. 在已经加载进来的libc.so.6里面找到printf函数真实内存地址
  2. 把这个地址回填到你程序预留的空坑位置。

这个动作就叫重定位 。 回填完成之后,你的程序才真正知道printf到底在哪,才可以调用 printf。

第 5 步:全部库加载、地址回填完毕,你的主程序正式开始运行

3.2静态库

Linux 静态库后缀:.a ,Windows 静态库 .lib

本质:把一堆 .o 目标文件打包压缩到一个归档文件里面 ,不是可执行程序,也不是机器码直接运行的文件,只是.o的集合包。

静态库完整流程

  1. 编写库源码 → 汇编生成 .o
  2. 使用 ar 工具,把多个 .o 打包成为静态库 libxxx.a
  3. 编译链接阶段 :链接器从 .a 包里把需要的 .o 目标文件拷贝复制出来,合并到你的可执行程序。

⚠️重点:运行阶段不再需要 .a 文件!代码已经复制进程序内部。


静态库libxxx.a本质是一堆.o目标文件的压缩包。 当链接器遇到 -lfunc:

  1. 打开libfunc.a归档包;
  2. 在包里面找出主程序需要用到的那个 .o目标文件;
  3. 把这个.o完整拷贝出来,合并拼装到最终可执行程序内部;
  4. 符号解析、重定位全部在链接阶段一次性做完。

⚠️注意:不是把整个.a全部复制,只拷贝你程序用到的.o模块。

链接结束: ✅ 函数机器码已经实实在在复制进可执行文件。 ✅ 所有函数地址全部算好,填完毕。 ❌ 之后运行,完全不再访问 libfunc.a。

静态库 .a 动态库 .so
本质 ar归档:零散.o的压缩包,没有经过链接 经过gcc -shared链接生成的完整 ELF 共享二进制
内部 保留一份份原始独立.o 已经合并,不存在原始.o
生成工具 ar rcs(只是打包,不链接) gcc -shared(执行链接)
使用时机 编译链接阶段 抽取.o拷贝到程序 运行时操作系统加载整个 so 文件到进程内存

误区:"动态库就是另一种打包的.o 集合" 错误。

静态库 = 打包的原材料(.o);

动态库 = 已经加工装订完成的成品书。

3.3故事理解动静态库

背景设定

小明要写一份项目报告(你的 C 语言源代码),报告里面很多地方要用到「标准计算公式」(库函数:printf、加法工具等)。 计算公式写在一本工具书《公式大全》(库)。

工具书有两种版本:纸质复印本(静态库 .a) 、图书馆公共藏书(动态库 .so)


📕静态库模式(静态链接)

链接 = 复印整本工具书,装订进自己的报告里

小明写报告的时候,为了方便,直接把《公式大全》整本书全部复印,把复印页全部钉进自己的报告文档里面。

✅好处:

  1. 这份报告本身什么都齐全。把报告拷贝给任何人,不需要再找原版工具书,拿过去直接看直接用。(程序不依赖外部库,移植方便)
  2. 使用的时候不用跑图书馆,直接翻自己报告里面的复印页面。

❌坏处:

  1. 报告变得很厚,体积巨大。(可执行文件体积大)
  2. 如果小红、小刚也做同样的报告,每个人都复印一份完整《公式大全》。每个人的报告里面都有一模一样的公式副本,大量纸张浪费。(内存冗余,每个程序都自带一份库代码)
  3. 如果出版社更新了《公式大全》出第二版。小明手里报告依旧是旧的复印版本,想要新公式,必须重新复印,重新装订报告。(库更新,程序必须重新编译链接)

对应命令:gcc test.c -o app_static -static


📚动态库模式(动态链接,Linux 默认)

链接:不在报告里抄公式,只在报告上写一行备注:「需要,请去图书馆借阅《公式大全》」

小明写报告,不复印任何公式,只在报告上标记:我需要用到《公式大全》这本书。

  • 写报告(编译链接阶段):只登记我需要哪本书,不把书拿过来。
  • 等到真正要阅读这份报告的时候(程序运行时),才派人去图书馆,把公共的《公式大全》借过来使用。

✅好处:

  1. 小明的报告薄薄的,体积很小。(可执行文件体积小)
  2. 小明、小红、小刚,所有人的报告都共用图书馆同一本原版《公式大全》。图书馆只存放一本,节省大量纸张。(内存中只加载一份动态库,多进程共享,节省内存)
  3. 出版社更新《公式大全》第二版,直接替换图书馆那本书就完事。所有人下次打开报告,自动使用新版公式,不需要每个人重新修改自己的报告。(替换 so 库,程序无需重编译)

❌坏处:

  1. 报告离开了图书馆就废掉! 如果图书馆把《公式大全》弄丢了,这份报告直接没法看。(运行机器必须存在对应的.so动态库,缺失库直接运行失败)
  2. 每次看报告,要多跑一趟图书馆借书,多一点小小的时间开销。(程序启动有加载库的开销)

对应命令:gcc test.c -o app_dynamic 默认动态链接。


故事对应考点小结

  1. 静态链接:链接阶段复制库代码到程序内部;自带全部内容,体积大,不依赖外部文件。
  2. 动态链接:链接只做记录,运行时才加载公共库;体积小,多程序共享库;依赖外部 so 文件。

一句话记忆

  • 静态:把书抄进自己本子,走到哪带到哪,本子很厚。
  • 动态:书放在公共图书馆,本子只写借书备注,用的时候去借。本子薄,但不能没有图书馆。

3.4动静态库的优缺

对比维度 静态库 .a 动态库 .so
文件本质 .o目标文件归档包,不做链接 经过链接生成完整 ELF 共享二进制
代码处理 链接时拷贝用到的.o到程序内部 链接仅记录依赖;运行时加载完整 so
运行依赖 不依赖.a,链接完可删除静态库 必须存在.so库文件
程序大小 大 小
内存表现 每个进程一份库代码,内存冗余 多进程共享内存一份库,节省内存
库升级 库更新,程序必须重新编译链接 替换 so 文件,程序无需重编译
制作工具 ar rcs gcc -fPIC -shared
是否需要‑fPIC 不需要 必须
启动性能 略快 略有加载开销
部署移植 简单,直接拷贝程序 复杂,要保证目标机器有对应 so

Q:静态库程序运行的时候会读取 libxxx.a 吗?

A:不会。链接阶段已经把代码拷贝到可执行程序,运行和.a无关。

Q:我把静态库删掉,程序还能跑吗?

A:可以正常运行。
动静态库判断题(附答案解析)

判断对/错

  1. 静态库 .a 是经过链接生成的ELF可执行文件。()
  2. 使用静态库的程序运行时,仍然需要本机存在.a文件才能正常运行。()
  3. 静态链接时,链接器会从.a归档包提取用到的.o,拷贝合并进可执行程序。()
  4. 动态库.so只是多个.o简单打包,没有做链接处理。()
  5. 动态链接程序,编译链接阶段会把so库的机器码复制到可执行文件内。()
  6. 多个进程使用同一个动态库,物理内存中只会保存一份该动态库代码。()
  7. 更新动态库,替换新版.so后,原有程序必须重新编译链接才能生效。()
  8. 制作动态库必须加 -fPIC 参数,制作静态库不需要。()
  9. ld‑linux‑x86‑64.so.2 动态链接器只负责静态链接程序。()
  10. 编译时‑L.参数,可以让程序运行时自动在当前目录找so动态库。()

答案+解析

  1. ❌错 解析:.a是ar打包的.o归档包,没有做链接 ;.so才是链接完成的ELF共享文件。
  2. ❌错 解析:静态库只参与链接阶段;链接完成,.a可以直接删除,运行完全不需要。
  3. ✅对 解析:静态库核心行为,抽取需要的目标文件拷贝到可执行程序。
  4. ❌错 解析:gcc -shared会执行链接,把多个.o合并生成完整so,不是简单打包。
  5. ❌错 解析:动态链接不拷贝库代码,仅记录依赖关系;运行时才加载so。
  6. ✅对 解析:动态库核心优势,内存共享,节约物理内存。
  7. ❌错 解析:动态库直接替换so即可,不需要重编译程序;静态库更新才需要重编译。
  8. ✅对 解析:‑fPIC生成位置无关代码,动态库必备;静态库无此要求。
  9. ❌错 解析:ld‑linux是运行时动态链接器,只管动态链接程序;静态链接程序不使用它。
  10. ❌错 解析:‑L仅作用于编译链接阶段 ;运行时查找so看LD_LIBRARY_PATH、ld.so.cache等,和‑L无关。

四.问题归纳

问题一:条件编译与if语句的区别

#ifdef:预处理指令 ,前面带#,发生在预处理阶段(编译之前)

if:C 语言语句,发生在程序运行阶段

对比项 条件编译 #ifdef / #endif 普通 if‑else
执行时机 预处理阶段,编译之前 程序运行的时候
处理方式 不满足条件的代码,直接被删掉,不会进入最终程序 所有代码全部编译进可执行文件,运行做判断
生成程序内容 被裁剪的代码,程序里面根本不存在 全部代码都存在,只是不一定执行
占用程序体积 无效代码直接丢弃,不占体积 全部代码都占程序体积
判断依据 宏是否定义,预处理时就确定 运行时变量、表达式的值

判断:使用#ifdef,条件不成立,里面的代码只是不会执行,仍然存在可执行文件中。(❌错)

解析:预处理直接删掉,产物中不存在该代码。

相关推荐
Industio_触觉智能1 小时前
瑞芯微RK系列调试工具与量产测试手册-上|串口日志打通与JTAG兜底调试
linux·sdk·jtag·瑞芯微·调试工具·rk3576·量产调试
Decay__7 小时前
05_MySQL 常用 SQL 大全 —— 诊断、管理、优化辅助 SQL 与三大系统库速查
运维·数据库·sql·mysql·架构
贞明电子9 小时前
工业三色报警灯怎么接入项目系统?开关量与RS485控制应用指南
运维·网络·微信公众平台·贞明电子
HideF10 小时前
Linux
linux·服务器·网络
万联WANFLOW10 小时前
海外网络故障怎么排查?Cloudflare 三季度断网报告解读与 mtr 实战
运维·网络·故障分析
Yuzhiyuxia12 小时前
【AI Agent面试系列】AI Agent记忆机制知识点1
人工智能·面试·职场和发展
Ruiery12 小时前
Linux 6.6内核内存管理深度解析(十三):slab/slub 分配器
linux·运维·服务器
乌恩大侠13 小时前
从 AI-RAN 到 AI Agent:基于 NVIDIA Nemotron 构建 5G/6G 智能网络故障诊断系统
服务器·人工智能·5g·o-ru·ai-ran·电信大模型
Decay__13 小时前
04_MySQL DBA 日常运维手册 —— 会话管理、锁与阻塞、事务监控、慢查询与日志巡检
运维·数据库·sql·mysql·架构