🦄 个人主页 : 小米里的大麦-CSDN博客
🎏 所属专栏 : Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页 : 小米里的大麦的 GitHub
⚙️ 操作环境 : Visual Studio 2022

文章目录
-
- [Linux 开发工具(上)](#Linux 开发工具(上))
-
- [Linux 编辑器 ------ `vim`](#Linux 编辑器 ——
vim
) -
- [Vim 的主要模式:](#Vim 的主要模式:)
- [Vim 命令模式下的一些常用操作: [史上最全 Vim 快捷键键位图(入门到进阶)](https://www.runoob.com/w3cnote/all-vim-cheatsheat.html)](#Vim 命令模式下的一些常用操作: 史上最全 Vim 快捷键键位图(入门到进阶))
- [Vim 多文件编辑与窗口管理](#Vim 多文件编辑与窗口管理)
- [简单的 `vim` 配置([Vim 改装](https://blog.csdn.net/wooin/article/details/1858917))](#简单的
vim
配置(Vim 改装))
- [解放 `sudo` 权限](#解放
sudo
权限) - [Linux 编译器 ------ `gcc `/ `g++`](#Linux 编译器 ——
gcc
/g++
) -
- [背景知识回顾 ------ C/C++ 程序的编译流程](#背景知识回顾 —— C/C++ 程序的编译流程)
- [1. 预处理 (Preprocessing)](#1. 预处理 (Preprocessing))
- [2. 编译 (Compilation)](#2. 编译 (Compilation))
- [3. 汇编 (Assembly)](#3. 汇编 (Assembly))
- [Linux 编辑器 ------ `vim`](#Linux 编辑器 ——
- 传道解惑
-
- Q1:什么是可重定位目标文件?
- Q2:为什么需要可重定位目标文件?
- Q3:什么时候会被最终链接?
-
- [4. 链接 (Linking)](#4. 链接 (Linking))
- [为什么我们能在 Windows 或 Linux 上进行 C/C++ 开发?](#为什么我们能在 Windows 或 Linux 上进行 C/C++ 开发?)
- [软件版本管理:社区版 vs 专业版](#软件版本管理:社区版 vs 专业版)
- 关键命令与文件类型总结
- 记忆法
- 函数库的概念与作用
- [Linux 中的可执行文件链接方式:动态、静态与混合链接](#Linux 中的可执行文件链接方式:动态、静态与混合链接)
- 小故事理解动静态链接
-
- [1. 默认情况下,gcc 采用动态链接](#1. 默认情况下,gcc 采用动态链接)
- [2. 强制使用静态链接(`-static` 选项)](#2. 强制使用静态链接(
-static
选项)) - [3. `-static` 的本质:改变库的优先级](#3.
-static
的本质:改变库的优先级) - [4. 混合链接(部分静态,部分动态)](#4. 混合链接(部分静态,部分动态))
- [5. `-static` 的优缺点](#5.
-static
的优缺点) - [6. 推荐场景](#6. 推荐场景)
- [Debug 与 Release 模式 & ELF 可执行文件格式](#Debug 与 Release 模式 & ELF 可执行文件格式)
-
- [1. Debug 模式(调试模式)](#1. Debug 模式(调试模式))
- [2. Release 模式(发布模式)](#2. Release 模式(发布模式))
- [3. ELF(Executable and Linkable Format,可执行文件格式)](#3. ELF(Executable and Linkable Format,可执行文件格式))
- 总结
- 共勉
Linux 开发工具(上)
Linux 编辑器 ------ vim
Vim 是一个强大、多模式的文本编辑器,具有高度的可定制性和丰富的功能。打开文件 :在命令行中输入 vim filename
,打开指定文件。Vim 从入门到牛逼 / Vim 从入门到牛逼(备用)
打开当前目录下的文件 code.c
,使用命令:vim code.c
,同时 支持相对路径和绝对路径 打开:vim home/code.c
。
Vim 的主要模式:
- 命令模式(Normal Mode) :命令模式是 Vim 的核心模式,几乎所有操作都从这里开始,启动 Vim 后,默认处于命令模式 。在此模式下,键盘输入被解释为命令而非文本输入 。只能使用方向键、删除、复制、粘贴等操作用来进行文本的导航、选择、删除、替换等。(按
i
进入插入模式,按:
进入底行模式,按Esc
从其他模式返回到命令模式) - 插入模式(Insert Mode) :按下
i
键进入插入模式。插入模式下可以像普通文本编辑器一样输入文本。按Esc
键返回命令模式。 - 底行模式(Ex Mode 或 Bottom Line Mode) :底行模式是 Vim 的强大功能之一,支持复杂的文件操作和配置。按下
shift
+;
(也就是:
) 键进入底行模式。在底行模式下可以执行文件保存、退出、查找替换等命令。例::wq
保存并退出 Vim。按Esc
键返回命令模式。(注意:Ctrl+C
也可以退出底行模式并返回命令模式,尽量不要使用,可能会出现未知错误)
Vim 命令模式下的一些常用操作: 史上最全 Vim 快捷键键位图(入门到进阶)
导航
gg
: 定位光标到最开始行。shift+g
或G
: 定位光标到最结尾行。n+shift+g
或nG
: 定位光标到任意行------跳到指定行号n
。shift+$
: 定位光标到当前行的结尾。shift+^
: 定位光标到当前行的开始。w, b
: 光标按照单词进行行内移动,分别向右或向左移动一个单词。h, j, k, l
: 分别移动光标左、下、上、右。
文本编辑
(n)yy
: 复制光标所在行或指定行数的内容。(n)dd
: 剪切(删除)当前行或指定行数的内容。(n)p
: 粘贴n
(次数)行到光标所在下一行。u
: 撤销上一步操作。ctrl+r
: 撤销之前的撤销(重做撤销的操作)。
其他操作
shift+~
: 大小写转换。(n)r
: 对光标字符之后的n
个字符进行批量化替换。shift+R
: 进入替换模式,对内容进行整体替换 → 第四种模式。(n)x
: 对光标后面的n
个字符进行删除。:w
保存,:w!
强制保存,:q
退出,:q!
强制退出,:wq!
强制退出并保存
这两个命令 (shift+$
和 shift+^
) 被称为"锚点 ",因为它们将光标快速固定到行的特定位置------就像锚点固定物体的位置一样。在文本编辑器中,锚点 通常指的是可以用来快速定位或固定某个位置的标记。
为什么 vim
快捷键不像现在键盘的常规操作:主要是因为早期的键盘并不像现在这样有功能键(F1-F12)、方向键或数字小键盘。Vim 的许多快捷键都是为了适应这些限制而设计的。(可以查找一下那个时代的键盘 🤪)
Vim 多文件编辑与窗口管理
在 Vim 中,你可以同时打开多个文件,并使用 分割窗口 来进行并行编辑。
打开多个文件 ------ 垂直分割窗口 (vs
):
- 作用:在当前窗口 右侧 打开一个新的窗口,并加载指定的文件。
- 用法:
:vs file2.c
。示例:先打开file1.c
,然后在 Vim 中输入:vs file2.c
,即可在右侧创建一个窗口并打开file2.c
。
窗口切换 Ctrl+w w
:
- 作用:在多个窗口之间切换光标位置。
- 操作方式:按
Ctrl+w
,再按w
,光标会跳到下一个窗口。如果有多个窗口,重复Ctrl+w w
可以在所有窗口之间循环切换。
窗口内编辑 :光标在哪个窗口里面,就对哪一个窗口进行操作 ,你可以在当前窗口进行 插入、删除、复制等编辑操作,而不会影响其他窗口。
- 水平分割窗口:
:sp file2.c
- 调整窗口大小:
Ctrl+w +
(增加高度)、Ctrl+w -
(减少高度)。
简单的 vim
配置(Vim 改装)
Vim 的配置文件有两个主要位置,注意:一个用户配置一个 vim
文件,不会互相影响,不建议给 root
做配置,推荐用普通用户!
- 全局配置文件 :
/etc/vimrc
,对所有用户都生效。 - 用户私有配置文件 :每个用户可以在其主目录下创建
.vimrc
,仅对该用户生效。例如,/root/.vimrc
适用于root
用户。
如何修改 .vimrc
- 切换到自己的用户目录(确保在自己的 home 目录下):
cd ~
- 打开
.vimrc
文件(如果不存在,则创建):vim .vimrc
常用 Vim 配置
在 .vimrc
文件中添加以下配置:
- 设置语法高亮:
syntax on
,作用: 开启 Vim 的语法高亮功能,使代码有不同颜色,提高可读性。 - 显示行号:
set nu
,作用: 开启行号显示,在左侧显示每行的编号,方便定位代码行。 - 设置缩进的空格数:
set shiftwidth=4
,作用: 设定每次缩进的空格数为 4。适用于代码自动缩进,提高代码可读性。
测试配置
修改 .vimrc
文件后,需要重新打开 Vim 或者在 Vim 内输入以下命令让配置生效::source ~/.vimrc
然后可以在 Vim 中测试:
- 输入
:set nu?
检查行号是否开启。 - 输入
:set shiftwidth?
检查缩进空格数。
这样就完成了简单的 Vim 配置! 🚀
使用插件(进阶)
Vim 支持插件扩展功能,要配置好看的 vim
,原生的配置可能功能不全,可以选择安装插件来完善配置,保证用户是你要配置的用户。Vim 的插件管理工具如 Vim-Plug,一些常用的插件如 NERDTree、coc.nvim 等,以增强 Vim 的功能。
推荐使用 Vim-Plug 作为插件管理器。
-
安装 Vim-Plug:
bashcurl -fLo ~/.vim/autoload/plug.vim --create-dirs \ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
-
配置插件 :在
.vimrc
中添加以下内容:bashcall plug#begin('~/.vim/plugged') Plug '插件名称' " 例如:Plug 'preservim/nerdtree' call plug#end()
-
安装插件 :
在 Vim 中运行
:PlugInstall
安装配置的插件。
安装方法: 在 shell
中执行指令(想在哪个用户下让 vim
配置生效,就在哪个用户下执行这个指令。强烈 "不推荐" 直接在 root 下执行),需要按照提示输入 root
密码。您的 root
密码不会被上传,请放心输入。
bash
curl -sLf https://gitee.com/HGtz2222/VimForCpp/raw/master/install.sh -o ./install.sh && bash ./install.sh
注意:安装完成后一定要手动执行 source ~/.bashrc
或重启终端,这样配置才能生效! 说明:安装完成后,目录下会有一个名为 install.sh
的配套脚本,处于安全考虑,可以执行 mv install.sh .install.sh
进行隐藏。
默认缩进配置为 2
个字符,将 tab
键换成 4
个字符的方法:到达家目录下执行 vim .vimrc
,大概在 50 行进行修改:
bash
set tabstop=4 " 每个制表符占用 4 个空格
set softtabstop=4 " 插入制表符时插入 4 个空格
set shiftwidth=4 " 自动缩进和文本块操作时使用 4 个空格
卸载方法: 在安装了 VimForCpp 的用户下执行:
bash
bash ~/.VimForCpp/uninstall.sh
解放 sudo
权限
使用 root
账户执行命令:
bash
vim /etc/sudoers
按下 i
键进入插入模式,找到大约第 100 行左右的位置(附近会有 root ALL=(ALL) ALL
的字眼),在其下方添加以下内容:
bash
username ALL=(ALL) ALL
username
:替换为你要授权的账户名。ALL=(ALL)
:允许用户以任何用户身份执行命令。ALL
:允许执行所有命令。
按 Esc
键返回到默认模式,然后输入 :wq!
强制保存并退出。
验证 :切换到该用户并测试 sudo
权限:
bash
su - username
sudo ls /root
如果 sudo
配置正确,系统会提示输入密码,然后执行命令。
Linux 编译器 ------ gcc
/ g++
背景知识回顾 ------ C/C++ 程序的编译流程
C/C++ 程序从源代码到可执行文件需经历四个主要阶段:预处理 → 编译 → 汇编 → 链接。每个阶段由编译器(如 GCC、Clang 或 MSVC)逐步处理,最终生成可执行文件或库文件。
1. 预处理 (Preprocessing)
预处理是编译的第一步,主要处理源代码中的宏、注释、头文件等。预处理的输出是一个经过宏替换、去注释、头文件展开的中间代码。
主要工作:
- 去除注释:删除代码中的注释部分。
- 宏替换 :替换代码中的宏定义(如
#define
)。 - 头文件展开 :将
#include
指令包含的头文件内容插入到当前文件。 - 条件编译 :根据条件编译指令(如
#ifdef
、#endif
)选择性地包含或排除代码。
输出:
- 预处理后的文件通常是一个
.i
文件,它是源代码经过宏替换和头文件展开后的中间结果。
命令示例:
bash
gcc -E temp.c -o temp.i # 执行预处理后就停止,`-E` 选项表示只进行预处理,输出 `.i` 文件。
重要说明: 预处理后的文件仍然是 C 语言代码,因此可以说经过预处理后,程序依然是 C 语言程序。
2. 编译 (Compilation)
编译阶段的作用是将预处理后的代码转化为汇编代码。编译器会对代码进行语法分析、语义分析和优化,最终生成与平台架构相关的汇编代码。
主要工作:
- 语法分析:检查代码是否符合语法规则。
- 语义分析:检查变量类型、函数调用等语义是否正确。
- 优化:对代码进行优化,使得生成的汇编代码更高效。
输出:
- 编译后的文件是一个
.s
文件,包含了平台特定的汇编代码。
命令示例:
bash
gcc -S temp.i -o temp.s # 执行编译后就停止,`-S` 选项表示将预处理后的文件编译成汇编语言。
3. 汇编 (Assembly)
汇编阶段的作用是将汇编代码转换为机器可识别的二进制目标代码。汇编器会将汇编代码翻译为机器指令,生成可重定位目标文件。
主要工作:
- 汇编:将汇编代码转换为目标代码,生成二进制文件。
输出:
- 汇编后的文件是一个
.o
(Linux)或.obj
(Windows)文件,这些文件是目标文件,包含机器指令,但尚未链接成最终的可执行文件。
命令示例:
bash
gcc -c temp.s -o temp.o # 执行汇编后停止,`-c` 选项表示只进行汇编,不进行链接,输出 `.o` 文件。
重要说明: 可重定位目标二进制文件,简称目标文件,.obj
文件不可以独立执行,虽然已经是二进制了,还需要经过链接才能执行!
传道解惑
在 Linux 中,可重定位目标二进制文件(Relocatable Object File)是一种 中间二进制文件 ,它是由编译器或汇编器生成的,但还 不能直接执行 ,必须经过 链接(Linking) 处理后才能成为最终的可执行文件或库文件。
Q1:什么是可重定位目标文件?
可重定位目标文件通常以
.o
结尾(Windows 上是.obj
),它包含:
- 机器指令(Machine Instructions):程序的可执行代码,但未指定最终内存地址。
- 符号表(Symbol Table):记录函数、变量等符号,供链接器解析。
- 重定位信息(Relocation Information):用于在链接时调整地址。
- 节(Section)结构 :比如
.text
(代码段)、.data
(已初始化数据段)、.bss
(未初始化数据段)等。这些文件是 "可重定位" 的,因为它们的地址信息 尚未固定 ,而是由 链接器 在合并多个目标文件时决定最终的内存布局。
Q2:为什么需要可重定位目标文件?
如果编译器直接把 C/C++ 源代码编译成最终可执行程序,那么:
- 无法将不同的模块(文件)合并,无法进行大规模项目开发。
- 无法使用动态链接库(Shared Library),会导致程序体积庞大。
- 无法延迟地址分配,不适用于操作系统的内存管理策略。
所以,编译器通常 先 生成 可重定位目标文件 ,再由 链接器 进行 地址调整和符号解析,最终得到可执行程序。
示例:生成可重定位目标文件
假设我们有一个简单的 C 代码
temp.c
:
c#include <stdio.h> void hello() { printf("Hello, World!\n"); }
第一步:编译但不链接
bashgcc -c temp.c -o temp.o
这样会生成
temp.o
,它是一个 可重定位目标文件 ,里面的hello
还没有绑定到最终的地址。第二步:使用
readelf
查看目标文件
bashreadelf -h temp.o
部分输出:
Type: REL (Relocatable file) Machine: x86-64
可以看到
Type
是REL
,表示它是一个 可重定位文件。Q3:什么时候会被最终链接?
可重定位目标文件 通常不会单独使用,它最终会:
- 静态链接(Static Linking) :被
ld
(链接器)合并到最终的可执行文件中。- 动态链接(Dynamic Linking) :与共享库(
.so
文件)进行链接,在运行时加载。例如:
bashgcc temp.o -o temp ./temp
这时
temp.o
经过链接后变成可执行文件temp
,可以直接运行。
4. 链接 (Linking)
链接阶段的作用是将多个目标文件(如 .o
文件)和库文件进行链接,生成最终的可执行文件或库文件。链接器会处理符号引用、重定位等工作,确保程序能够正确运行。
主要工作:
- 符号解析:将不同目标文件中的符号(如函数名、变量名等)进行链接,确保各部分之间的正确引用。
- 重定位:调整代码中地址的引用,使得目标文件能够在内存中正确加载。
输出:
- 链接后的输出通常是一个可执行文件(如
a.out
或program.exe
)或动态链接库(如.so
或.dll
文件)。
命令示例:
bash
gcc source.o -o program # 默认链接 C 标准库,这条命令将目标文件 `temp.o` 链接为可执行文件 `program`。
为什么我们能在 Windows 或 Linux 上进行 C/C++ 开发?
在 Windows 和 Linux 上进行 C/C++ 开发是因为这些操作系统提供了支持编译和执行 C/C++ 代码的工具链。具体而言,需要安装开发环境(如 GCC、Visual Studio 等)以及相关的库文件和头文件。
1. 头文件和库文件
- 头文件 :定义了语言的核心功能、标准库以及外部库的接口(如
stdio.h
、stdlib.h
)。 - 库文件 :包含预编译的二进制代码(如
libc.a
或msvcrt.dll
),在链接阶段与用户代码结合。
2. 开发工具
- 编译器:如 GCC(GNU Compiler Collection)、g++,以及 Visual Studio(VS)等 IDE,提供了代码编辑、编译、调试等功能。
- 语言支持包 :安装开发工具时,会自动下载头文件和库。 例如,安装 VS 时选择"C++ 开发",会附带 C++ 标准库(如 STL)和 Windows API 库。
3. 跨平台开发
- 条件编译 :通过宏(如
#ifdef __linux__
)区分不同平台的代码逻辑。 - 平台专属库 :Linux 依赖
glibc
,Windows 依赖msvcrt.dll
。开发者需调用标准接口或使用跨平台库(如 Qt、Boost)。
软件版本管理:社区版 vs 专业版
1. 代码维护策略
-
单一代码库 :企业无需维护多份代码,而是通过 条件编译 控制功能模块的启用或禁用。例如:
c#ifdef PROFESSIONAL_EDITION enableAdvancedFeatures(); #endif
-
编译参数控制 :构建时通过宏定义(如
-DPROFESSIONAL_EDITION
)选择版本。
2. 功能裁剪 ------ 不需要维护两份代码,根据不同的编译条件,会裁剪掉社区版不需要的功能即可
- 社区版:禁用部分高级功能(如性能分析工具、企业级加密)。
- 专业版:包含完整功能,通过编译选项开启。
关键命令与文件类型总结
阶段 | 命令选项 | 输入文件 | 输出文件 | 文件内容 |
---|---|---|---|---|
预处理 | -E |
.c |
.i |
展开后的 C 代码 |
编译 | -S |
.i |
.s |
汇编代码 |
汇编 | -c |
.s |
.o (Linux) |
目标二进制文件 |
链接 | 无 | .o |
a.out (默认) |
可执行文件 |
记忆法
-
ESC 键记忆法:键盘左上角的 ESC 键可以关联各个阶段的后缀:
- .i -> 预处理(Preprocessing)
- .s -> 汇编(Assembly)
- .o -> 目标文件(Object file)
-
iso → 镜像文件后缀。 最终的可执行文件或库文件就是链接的结果。
cpp编译流程: .c 源文件 → 预处理 → 编译 → 汇编 → 链接 → 可执行文件 (-E) (-S) (-c) 多文件编译例如: processBar.c → processBar.o ─┐ ├→ processBar processBarmain.c → processBarmain.o ─┘
函数库的概念与作用
在 C/C++语言编程中,函数库(Library)允许开发者复用已有的代码,而不必每次都从头开始编写。函数库通常包含一组预先编写好的函数,这些函数可以被多个程序调用。通过使用函数库,开发者可以节省时间,减少重复劳动,并提高代码的可靠性和可维护性。
函数库的分类
函数库主要分为两种类型:静态库 和 动态库。
1. 静态库(Static Library)
- 定义:静态库在编译链接时,会将库文件的代码全部加入到可执行文件中。因此,生成的可执行文件会比较大,但在运行时不再需要库文件。
- 后缀名 :静态库的后缀名通常为
.a
(例如libmylib.a
)。 - 优点:由于库代码被直接嵌入到可执行文件中,程序运行时不需要依赖外部的库文件,因此具有较好的独立性。
- 缺点:生成的可执行文件较大,且如果多个程序使用相同的静态库,每个程序都会包含一份库代码的副本,导致内存浪费。
2. 动态库(Dynamic Library)
- 定义:动态库在编译链接时并不会将库文件的代码加入到可执行文件中,而是在程序运行时由操作系统的运行时链接器动态加载。这样可以节省系统的开销。
- 后缀名 :动态库的后缀名通常为
.so
(例如libmylib.so
)。 - 优点:生成的可执行文件较小,多个程序可以共享同一个动态库,节省内存和磁盘空间。此外,动态库可以在不重新编译程序的情况下更新。
- 缺点:程序运行时需要依赖外部的库文件,如果库文件丢失或版本不兼容,程序可能无法运行。
函数库的命名规则
函数库的命名通常遵循一定的规则,以便于识别和使用。常见的命名格式为:
libname.so.XXX
lib
是前缀,表示这是一个库文件。name
是库的名称,例如c
表示 C 标准库。.so
表示这是一个动态库(静态库通常使用.a
)。XXX
是版本号,表示库的版本。
例如,libc.so.6
是 C 标准库的动态库,版本号为 6。
函数库的使用
在 C 语言中,常用的函数库如 printf
函数的实现并没有直接包含在源代码中,也没有在头文件 stdio.h
中定义。那么,这些函数是如何被调用的呢?
1. 头文件的作用 :头文件(如 stdio.h
)中只包含了函数的声明(即函数原型),告诉编译器这些函数的存在及其参数和返回值的类型。头文件并不包含函数的实现。
2. 库文件的作用 :函数的实现通常位于库文件中。例如,printf
函数的实现位于 C 标准库的动态库 libc.so.6
中。编译器在链接阶段会将这些库文件与代码结合起来,生成最终的可执行文件。
3. 链接过程
- 静态链接:在编译时,静态库的代码会被直接嵌入到可执行文件中。生成的可执行文件不依赖外部的库文件。
- 动态链接:在编译时,动态库的代码不会被嵌入到可执行文件中。程序运行时,动态链接器会根据预定义的路径规则去查找这些库文件。
函数库的作用 ------ 不让我们做重复工作,站在巨人的肩膀上享受
- 代码复用:避免重复造轮子,开发者可以直接使用封装好的函数,而不用自己实现复杂的功能。
- 隐藏源码:共享库只提供
.so
文件,而不公开.c
源文件,保护代码的知识产权。 - 提高程序效率:通过动态链接的方式,多个程序可以 共享同一份库文件,减少内存占用,提高运行效率。
Linux 中的可执行文件链接方式:动态、静态与混合链接
在 Linux 中,gcc
编译形成可执行程序时,默认采用动态链接 ,即程序运行时依赖 .so
(共享库),这样可以减少可执行文件的大小,并允许多个程序共享相同的库文件。然而,我们可以通过 静态链接 或 混合链接 的方式来改变默认的链接行为。
小故事理解动静态链接
想象一下,你正在设计一款可以快速适应不同地形的汽车。为了实现这一目标,你决定使用一些通用的动态链接库,比如"越野轮子"、"城市轮胎"和"雪地链"。这些组件可以在不同的车型之间共享,并且可以根据需要在车辆启动时或行驶过程中即时替换。如果一个顾客今天想要一辆适合城市驾驶的汽车,你可以安装"城市轮胎";如果明天他想去越野冒险,只需更换为"越野轮子"。这种灵活性就像动态链接(库),程序运行时按需加载,多个程序共享,节省内存,灵活高效。
另一方面,有些汽车组件一旦安装就不能轻易更改了,例如车身结构或者发动机。这些是根据特定需求量身定做的。当你选择了一种类型的发动机后,它就永久性地成为汽车的一部分,无法在不拆卸整个汽车的情况下进行更换。静态链接库就像发动机,在编译时整合进程序,成为其固定组成部分,提供稳定功能,但程序体积较大,独立运行。
1. 默认情况下,gcc 采用动态链接
当我们编译 C 语言程序:
bash
gcc hello.c -o hello
默认情况下,gcc
优先链接动态库 (.so
),并在执行时加载库文件。例如,标准 C 库 libc.so.6
是动态链接的:
bash
ldd hello
示例输出:
bash
linux-vdso.so.1 => (0x00007fffca9fe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2a4c3c9000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2a4c788000)
这表示 hello
依赖 libc.so.6
,而不是 libc.a
(静态库)。
2. 强制使用静态链接(-static
选项)
如果想让程序采用 纯静态链接 ,可以使用 -static
选项:
bash
gcc hello.c -o hello -static
这时,编译器会 优先查找并链接静态库(.a
) ,把所有库代码嵌入可执行文件中,使其不再依赖外部 .so
文件。检查:
bash
ldd hello
如果是纯静态链接,输出会是:
plaintext
not a dynamic executable
这表明 hello
不依赖任何动态库。
3. -static
的本质:改变库的优先级
使用 -static
选项时,必须保证所有依赖库的静态版本(.a
)都存在 ,否则,编译会失败。如果没有静态库,但强制使用 -static
会报错!如果系统只有静态库,没有动态库 ,即使没有 -static
,gcc
仍然会成功编译,并采用静态链接。因为它找不到 .so
文件,只能用 .a
。
- 默认情况下 ,
gcc
优先选择动态库(.so
)。 - 加上
-static
,gcc
强制使用静态库(.a
),如果找不到静态库,则会报错。 - 并且
-static
是"一次性"的,即:- 所有 需要的库都必须静态链接。
- 不能部分使用动态库,部分使用静态库。
如果 -static
选项下某个库没有 .a
文件,编译就会失败。
4. 混合链接(部分静态,部分动态)
虽然 -static
会让所有库都静态链接,但我们可以通过手动指定库的链接方式,实现 混合链接(部分库静态,部分库动态)。
让特定库使用静态链接 :使用 -Wl,-Bstatic
指定部分库静态链接:
bash
gcc hello.c -o hello -Wl,-Bstatic -lcustomlib -Wl,-Bdynamic -lc
-Wl,-Bstatic
让-lcustomlib
采用静态库libcustomlib.a
。-Wl,-Bdynamic
让-lc
采用动态库libc.so
(恢复默认动态链接)。- 这样就实现了 混合链接(部分库静态,部分库动态)。
5. -static
的优缺点
方式 | 优点 | 缺点 |
---|---|---|
动态链接(默认) | - 可执行文件小,多个程序共享库,节省磁盘、内存、网络等空间 - 库可以独立更新,不需重新编译程序 | - 依赖外部 .so 文件,库缺失时程序无法运行 |
静态链接(-static) | - 程序可以独立运行,无需依赖库文件 - 适用于嵌入式系统或无共享库的环境 | - 可执行文件大(是动态的几十上百倍!),占用更多磁盘、内存、网络空间 - 无法通过库更新修复漏洞,需要重新编译 |
6. 推荐场景
场景 | 建议链接方式 |
---|---|
普通 Linux 应用 | 默认动态链接(节省空间,便于更新) |
需要独立运行的程序 | 静态链接(-static ),避免外部依赖 |
服务器软件 | 混合链接,关键库静态,其余动态 |
嵌入式系统 | 静态链接,减少依赖 |
Debug 与 Release 模式 & ELF 可执行文件格式
1. Debug 模式(调试模式)
- 包含调试信息(如变量名、函数符号表)。
- 没有优化,代码尽量保持源代码的执行逻辑,方便调试。
- 可追踪调试 ,可以用
gdb
(GNU Debugger)进行断点、变量查看等操作。
编译方式:
bash
gcc -g hello.c -o hello_debug
-g
选项:生成调试信息,方便gdb
进行调试。hello_debug
:可执行文件,但体积较大,因为包含了额外的调试信息。
调试方式:
bash
gdb ./hello_debug # 可以设置断点、查看变量值等。
2. Release 模式(发布模式)
- 优化代码,提高执行效率。
- 去除调试信息,减小可执行文件大小。
- 适用于正式发布的程序,但不方便调试。
编译方式:
bash
gcc -O2 hello.c -o hello_release
-O2
(优化等级 2):让编译器优化代码,提高执行速度(-O3
可进一步优化)。- 没有
-g
,不生成调试信息。
3. ELF(Executable and Linkable Format,可执行文件格式)
在 Linux 下,可执行程序形成的时候,不是无顺的二进制构成,可执行程序有自己的二进制格式 ------ ELF 格式。
- 程序头部(Program Header):描述如何加载程序。
- 代码段(.text):存放程序的可执行代码。
- 数据段(.data、.bss):存放全局变量、静态变量等。
- 符号表(仅 Debug 模式下存在):包含函数、变量等信息,方便调试器使用。
查看 ELF 结构:
bash
readelf -h hello_debug # 查看 ELF 头部信息
objdump -d hello_debug # 反汇编可执行文件
总结
GCC/G++ 在 Linux 下的常用编译选项
选项 | 作用 | 示例 |
---|---|---|
-o <文件名> |
指定输出的可执行文件名(gcc -o 输出文件 源文件 和 gcc 源文件 -o 输出文件 均可,更推荐后者!) |
gcc hello.c -o hello |
-c |
仅编译,不进行链接,生成 .o 目标文件 |
gcc -c hello.c -o hello.o |
-g |
生成调试信息,便于 gdb 调试 |
gcc -g hello.c -o hello_debug |
-O0 |
不优化,适用于 Debug | gcc -O0 hello.c -o hello |
-O1 |
基本优化,适用于一般调试 | gcc -O1 hello.c -o hello |
-O2 |
标准优化,提高性能,适用于 Release | gcc -O2 hello.c -o hello |
-O3 |
高级优化,可能影响可读性和调试 | gcc -O3 hello.c -o hello |
-Wall |
启用所有常见警告 | gcc -Wall hello.c -o hello |
-Wextra |
启用额外的警告信息 | gcc -Wall -Wextra hello.c -o hello |
-Werror |
把所有警告当作错误 | gcc -Werror hello.c -o hello |
-static |
进行 静态链接 ,不依赖 .so 库 |
gcc -static hello.c -o hello |
-shared |
生成 共享库(动态库) .so 文件 |
gcc -shared -fPIC hello.c -o libhello.so |
-fPIC |
生成位置无关代码(用于动态库) | gcc -fPIC -c hello.c -o hello.o |
-L<路径> |
指定库文件搜索路径 | gcc hello.c -L/usr/local/lib -lhello -o hello |
-I<路径> |
指定头文件搜索路径 | gcc hello.c -I/usr/local/include -o hello |
-l<库名> |
链接指定的库(默认搜索 /lib 和 /usr/lib ) |
gcc hello.c -lm -o hello (链接 libm.so 数学库) |
-pthread |
支持多线程编译 | gcc hello.c -pthread -o hello |
-std=<标准> |
指定 C 或 C++ 标准 | gcc -std=c99 hello.c -o hello |
-D<宏定义> |
定义宏,等效于 #define |
gcc -DDEBUG hello.c -o hello |
-E |
仅进行 预处理 ,输出 .i 文件 |
gcc -E hello.c -o hello.i |
-S |
仅进行 编译 ,输出汇编代码 .s 文件 |
gcc -S hello.c -o hello.s |
-v |
显示详细的编译过程 | gcc -v hello.c -o hello |
--version |
显示 gcc/g++ 版本 |
gcc --version |
G++ 额外的选项
选项 | 作用 | 示例 |
---|---|---|
-fno-rtti |
禁用运行时类型识别(RTTI) | g++ -fno-rtti hello.cpp -o hello |
-fno-exceptions |
禁用异常处理(try/catch ) |
g++ -fno-exceptions hello.cpp -o hello |
-std=c++11 |
使用 C++11 标准 | g++ -std=c++11 hello.cpp -o hello |
-std=c++17 |
使用 C++17 标准 | g++ -std=c++17 hello.cpp -o hello |
-std=c++20 |
使用 C++20 标准 | g++ -std=c++20 hello.cpp -o hello |
gcc
与 g++
的关系
-
g++
可以使用gcc
的全部选项 ,因为g++
本质上是gcc
的一个前端,专门用于编译 C++ 代码。 -
gcc
既可以编译 C 也可以编译 C++,但默认 不会自动链接 C++ 标准库。 -
g++
默认 会自动链接 C++ 标准库 (如libstdc++
),并开启 C++ 语法支持。
共勉