1. 为什么我把这四件套放在一起讲
很多刚接触Linux的朋友都有过这种经历:在Windows上写代码,装个IDE点一下"运行"就完事了。到了Linux环境突然懵了——软件从哪装?代码用什么写?写完了怎么编译?程序崩了怎么查?这些问题其实是同一个问题:你还没建立起Linux下的完整开发链路意识。
yum、vim、gcc/g++、gdb这四样东西,恰好完整覆盖了"装环境→写代码→编译构建→定位问题"这一整条开发流水线。yum帮你解决依赖和安装,vim让你能在终端里高效编辑文件,gcc/g++把你的代码变成可执行文件,gdb则在你程序崩溃或行为异常时帮你看到底层到底发生了什么。四者环环相扣,缺一个你都会在某个环节卡住。
这篇文章不是把man手册抄一遍,而是按实际开发流程组织的使用笔记。如果你刚学Linux、准备进入嵌入式或服务器开发方向,或者之前只在IDE里写代码、想理解底层工具链是怎么协作的,这篇内容能帮你少走不少弯路。我会把每个工具的使用逻辑讲清楚,再补上一些只有踩过坑才会知道的细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. yum:不要死记命令,先理解仓库和依赖这两个概念
yum是Linux发行版(尤其是Red Hat系,比如CentOS、Rocky Linux)的软件包管理器。它解决的核心痛点有两个:从哪下载和依赖怎么办。前者对应"仓库(repo)"的概念,后者是yum最值钱的自动化能力。
先说仓库。yum本身不存软件,它从配置好的软件源地址拉取。这个地址可以是公网镜像站,也可以是你自己搭的本地服务器。软件包带上依赖关系、版本号、校验信息等元数据,被组织成仓库,yum通过读取这些元数据来知道"装某个软件需要哪些前置包"。所以你在使用yum时会先执行一条更新缓存的操作,就是让它重新拉取仓库元数据,避免装到过时或根本不存在的版本。
2.1 换源操作:为什么我建议你拿到机器第一件事就做这个
默认官方的软件源服务器在海外,国内访问速度时快时慢,慢的时候装个几十MB的包都能等到怀疑人生。换源的本质就是把仓库地址指到国内镜像站,比如阿里云、清华、中科大提供的镜像。这一招对CentOS 7/8、Rocky Linux都适用。
先备份原本的repo配置,这一步养成习惯,出问题还能还原:
bash复制mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak
然后下载对应系统的镜像源配置。以阿里云为例,CentOS 7可以这样:
bash复制curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
注意这里的Centos-7.repo里的"Centos"拼写不带小写s,这个文件是阿里云打包好的现成配置,里面已经把base、extras、updates这些仓库路径都指向了阿里云镜像。下载后需要把配置里的$releasever变量确认一下——这个变量通常由系统自动解析成主版本号,比如7或8,但如果你自己改过release文件,这里可能就不对。检查方式是:
bash复制cat /etc/yum.repos.d/CentOS-Base.repo | grep releasever
正常会看到类似mirrors.aliyun.com/centos/$releasever/os/x86_64/这样的路径。如果你不确定,直接手动把$releasever替换成具体的7或8,省得出幺蛾子。
最后清理缓存并重新生成:
bash复制yum clean all
yum makecache
makecache执行时会看到每个仓库的下载进度,如果卡在某一处不动,八成是网络对那个镜像站不通。可以直接Ctrl+C中断,换另一个镜像源再试。个人强烈建议同一个系统只保留一个镜像站的repo文件,多个源混用版本不一致容易引起依赖解析冲突,得不偿失。
2.2 本地yum源:没网环境下的救命方案
有些公司内网服务器是完全隔离外网的,这时候yum没法从公网拉包。解决方案是配置本地yum源——把系统安装光盘ISO里的软件包目录挂载出来,让yum从这个本地目录读取。操作逻辑和换源一样,只是路径从HTTP变成了本地路径。
bash复制mkdir /mnt/cdrom
mount /dev/cdrom /mnt/cdrom # 把光盘挂载到目录
然后新建一个本地repo文件:
bash复制vi /etc/yum.repos.d/local.repo
写入以下内容:
ini复制[local]
name=local repo
baseurl=file:///mnt/cdrom
enabled=1
gpgcheck=0
重点注意gpgcheck=0这个配置。gpgcheck是包签名校验开关,原始ISO包里通常带有RPM-GPG-KEY文件用于校验,但如果你图省事不给它指定key文件,就把gpgcheck关掉,否则yum会报Public key for xxx is not installed之类错误。公网源一般建议保留gpgcheck=1以保证安全,本地可信源关掉影响不大。
设置完成后同样执行yum clean all && yum makecache。验证是否生效可以执行yum list,看到输出里出现了本地仓库的包列表就说明成功了。
2.3 常用操作:搜、装、卸、查,四类命令足够了
bash复制# 搜索包(不确定包名时很好用)
yum search 关键词
# 安装,-y表示自动回答yes
yum install -y 包名
# 卸载
yum remove 包名
# 查看已安装包
yum list installed | grep 包名
# 查看某个包的信息
yum info 包名
这里有个高频坑:yum安装时提示No package xxx available。别急着怀疑包名拼错,先刷新缓存。很多时候是新系统首次使用yum,元数据还没拉取完整,执行一次yum makecache再装就正常了。另外,某些软件不在默认仓库里,这时候需要先安装epel-release扩展仓库。
bash复制yum install -y epel-release
yum makecache
EPEL(Extra Packages for Enterprise Linux)是Fedora社区维护的扩展仓库,里面收录了大量默认源里没有的软件。装机装到最后发现总有几个包找不到,基本都是这个原因。
3. vim:它不是编辑器,是一套编辑语言
vim劝退过无数新手,因为它和Windows下的编辑器交互逻辑完全不一样。但一旦上手,效率确实高——因为它把"编辑"这件事拆成了三个阶段:普通模式看代码、命令模式做操作、插入模式写内容。你不需要频繁在键盘和鼠标之间切换,手可以一直停留在键盘上,这是它高效的根本原因。
3.1 模式切换是vimgateway,先过这一关
刚进入vim时处于普通模式(Normal Mode)。在这个模式下你输入的字符不会被写入文件,而是被解释为命令。按i进入插入模式(Insert Mode),此时才能正常打字。按Esc返回普通模式。按:进入命令模式(Command-Line Mode),可以执行保存、退出、搜索替换等命令。
新手最常见的死循环是:在插入模式下想保存文件,输入了"wq"四个字母,结果文件里多了四个字符。解决办法就一句话——先按Esc回到普通模式,再按冒号进入命令模式,最后输入wq回车。把这个流程刻进肌肉记忆,vim就算入门一半了。
保存退出相关的命令组合:
vim复制:wq " 保存并退出
:w " 只保存不退出
:q! " 不保存强制退出
:x " 文件有修改才保存并退出,无修改直接退出
q!这个组合极其重要,你乱改一通不想要的修改,全靠它救回来。但注意,vim会有交换文件机制——如果之前不正常退出,留下.swp文件,再打开同一个文件会提示Swap file already exists。这时候要么选择只读打开,要么删掉swp文件,命令是rm .文件名.swp。这个文件是隐藏文件,用ls -a才能看到。
3.2 日常高频操作:移动、删除、复制粘贴与批量操作
vim的操作命令很多,但日常开发真正高频率用到的其实就那十几个。记住下面这组,基本能覆盖百分之八十的场景。
移动光标(普通模式下)
h j k l:左、下、上、右(方向键也能用,但手不离主键盘区才高效)gg:跳到文件第一行G:跳到文件最后一行0:跳到行首$:跳到行尾Ctrl + d/Ctrl + u:向下/向上翻半屏
删除
dd:删除当前行x:删除光标所在字符dw:删除一个单词dG:删除当前行到末尾所有内容
复制粘贴
yy:复制当前行p:粘贴到光标下方P:粘贴到光标上方yw:复制一个单词
撤销与重做
u:撤销Ctrl + r:重做
批量操作是vim真正的效率所在。比如我要在1到10行行首都加上#注释,不需要逐行改:
vim复制:1,10s/^/#/
这条命令的意思是在第1行到第10行的行首(^匹配行首)替换为空内容加上#。同理,去掉注释就是把行首的# 替换为空:
vim复制:1,10s/^# //
s是替换命令(substitute),格式是[范围]s/查找内容/替换内容/[标志]。范围可以是具体的行号区间,也可以用%代表全文。这个命令是vim里最值得花时间学透的,因为它把批量编辑从重复劳动变成了单条命令。
3.3 文件内搜索与多文件切换的小技巧
在文件里搜索用/关键词,回车后按n跳到下一个匹配,N跳到上一个。配合set hlsearch开启高亮,匹配结果一目了然。关闭高亮用:nohlsearch。
同时打开多个文件的需求经常遇到。比较简单的方案是:
bash复制vim file1.c file2.c
默认显示第一个文件,用:n切换到下一个,:N切回上一个。但这种方式可能不太直观。我更喜欢用vim file1.c file2.c打开后,用:ls列出所有缓冲区,然后:b 编号跳转到对应文件——缓冲区列表的方式在多个文件之间跳转更灵活,尤其适合在头文件和源文件之间反复横跳的场景。
3.4 vim的配置固化:让每次打开都有好用的默认行为
每次打开vim都要手动设置set number显示行号太烦了,可以把配置写进用户目录下的.vimrc文件。没有这个文件就新建一个:
bash复制vim ~/.vimrc
我的基础配置如下:
vim复制set number " 显示行号
set relativenumber " 显示相对行号(配合dd/yy操作很好用)
syntax on " 语法高亮
set tabstop=4 " tab键显示的宽度
set shiftwidth=4 " 自动缩进的宽度
set expandtab " 用空格代替tab
set smartindent " 智能缩进
set hlsearch " 搜索结果高亮
set incsearch " 搜索时实时匹配
set wildmenu " 命令行补全增强
设置相对行号后,你会看到行号变成当前行是0,上下是1、2、3……这样配合数字加dd或yy特别方便。比如当前行往下隔三行要删掉,直接3dd就删了,不用自己去数绝对行号。
4. gcc/g++:从源码到可执行文件,四个阶段缺一不可
gcc(GNU Compiler Collection)是Linux下最核心的编译器。gcc对应C语言,g++对应C++。很多人以为编译器就是把源码"变"成可执行文件的黑盒子,其实它内部经历了预处理、编译、汇编、链接四个阶段。理解这四个阶段,你才能看懂那些编译报错到底发生在哪一步,才能解决"为什么我编译过了但运行报错"这类问题。
4.1 四个阶段分别干了什么
以C语言源文件hello.c为例:
第一阶段:预处理(Preprocessing)
这个阶段处理以#开头的预处理指令,比如#include把头文件的内容原样展开到源码里,#define做宏替换。可以单独执行这条命令查看预处理后的结果:
bash复制gcc -E hello.c -o hello.i
生成的hello.i文件会比原文件大很多,因为头文件内容都被"复制"进来了。如果你怀疑某个宏定义有问题,查看这个文件是排查的好办法。
第二阶段:编译(Compilation)
把预处理后的C代码翻译成汇编代码。注意这一步还不产生机器码,只是变成人可读的汇编。执行:
bash复制gcc -S hello.i -o hello.s
查看生成的.s文件,你会看到一堆mov、push之类的汇编指令。如果你了解汇编,看这一步能直观知道编译器把你的C代码"翻译"成了什么样的底层操作。
第三阶段:汇编(Assembly)
把汇编代码变成机器码,生成目标文件(object file):
bash复制gcc -c hello.s -o hello.o
hello.o已经是二进制了,用cat打开会是一堆乱码,但此时它还不能运行。你可以用file hello.o看看,它显示的是relocatable文件,说明还需要经过链接才能变成可执行文件。
第四阶段:链接(Linking)
把多个目标文件和系统库函数实现合并成一个可执行文件。这里解决的问题是:你的代码里调用了printf,但printf的实现并不在你的源码里,它躺在系统的C标准库中。链接阶段就是把这些依赖关系"接上":
bash复制gcc hello.o -o hello
出现名为hello的可执行文件后,直接./hello运行它。
4.2 实际开发中的常用参数组合
上面的四步拆分是为了理解原理。实际开发没人会一步步执行,通常一条命令搞定:
bash复制gcc hello.c -o hello
但有几个参数是必须养成的习惯:
-Wall开启所有常见警告
bash复制gcc hello.c -o hello -Wall
这个参数极其重要。C语言是一门"你不说清楚它就不拦你"的语言,很多潜在问题编译器默认不报,只有开启-Wall才会提示。比如定义了变量没用、函数声明不匹配等。我见过太多新手代码运行时行为诡异,加上-Wall一编译立刻暴露问题根源。
-g加入调试信息
bash复制gcc hello.c -o hello -g
这个参数是给下一章gdb准备的。不加-g也能调试,但你看不到源码行号、变量名,那种调试体验等于蒙着眼睛找针。在开发阶段,-Wall -g应该成为你的默认编译参数。
-O2优化等级
bash复制gcc hello.c -o hello -O2
优化等级从-O0(不优化,调试体验最好)到-O3(激进优化,运行速度最快)。开发调试阶段建议用默认的-O0,发布版本再用-O2或-O3。用-O2编译的程序,gdb里看到的变量值和源码逻辑可能对不上,因为优化器重排了指令,这是正常现象,别以为是自己代码写错了。
4.3 多文件编译与头文件搜索路径
真实项目很少是单个源文件。最常见的多文件编译方式:
bash复制gcc main.c utils.c -o app
编译器会把所有*.c文件分别编译再链接到一起。但更好的做法是分开编译目标文件再链接,这样修改一个文件只需要重新编译那一个:
bash复制gcc -c main.c -o main.o -Wall -g
gcc -c utils.c -o utils.o -Wall -g
gcc main.o utils.o -o app
这里第一行用了-c只生成目标文件不做链接。实际工程里这个流程交给后续要学的make/cmake自动完成,手动执行是为了理解底层的编译链。
如果头文件和源文件不在同一目录,编译器默认只会在当前目录和系统标准路径下找头文件。用-I参数指定头文件搜索路径:
bash复制gcc main.c -I./include -o app
链接时如果用到非系统默认路径下的库文件,用-L指定库文件搜索路径,-l指定库名。库名有一段经典的"去掉lib前缀、去掉.so后缀"规则:比如你想链接libm.so这个数学库,参数写作-lm:
bash复制gcc main.c -L./lib -lm -o app
这个规则不搞清楚,你会经常在编译链接阶段遇到cannot find -lxxx报错,其实不是库不存在,而是路径没指对或者命名不匹配。
5. gdb:不是所有bug都能靠printf解决
初学者排查程序问题最多的手段就是往代码里插printf,打印中间变量的值。这招在小程序里勉强能用,但一旦遇到程序崩溃、死循环、多线程竞争这类问题,printf就力不从心了——你根本不知道在哪插、插多少。gdb的价值在于:让程序停下来,逐行执行,随时查看任意变量的值,崩溃时直接告诉你挂在哪一行。
5.1 编译时记得加-g,否则后面全白搭
使用gdb的前提是编译时加上-g参数,这一步忘了,后面所有调试操作都会因为"没有调试信息"而受限。加上之后生成的可执行文件体积会变大,这是正常的。
启动调试:
bash复制gdb ./hello
进入gdb交互界面后会看到(gdb)提示符。此时程序还没有运行,你需要给它下命令才会执行。
5.2 最常用的一组调试命令
打断点
bash复制(gdb) break main # 在main函数入口打断点
(gdb) break 10 # 在第10行打断点
(gdb) break utils.c:25 # 在utils.c文件的第25行打断点
运行控制
bash复制(gdb) run # 运行程序,遇到断点停下
(gdb) continue # 继续运行到下一个断点
(gdb) next # 执行下一行(不进入函数内部,相当于step over)
(gdb) step # 执行下一行(进入函数内部,相当于step into)
(gdb) finish # 运行完当前函数后停下
(gdb) quit # 退出gdb
查看数据
bash复制(gdb) print 变量名 # 查看变量当前值
(gdb) info locals # 查看当前栈帧的所有局部变量
(gdb) display 变量名 # 每次停下都自动打印该变量
查看调用栈
bash复制(gdb) backtrace # 列出函数调用链
(gdb) frame 编号 # 切换到调用链中的某一层
backtrace是我用的最多的命令之一。程序崩溃时,先run复现,gdb会提示段错误发生位置,然后backtrace查看调用链,你能立刻看到是从哪一层函数一路调进来的,对定位递归崩溃、空指针悬空这类问题帮助立竿见影。
5.3 实战推演:一次段错误(Segmentation Fault)的排查过程
写一个简单的段错误程序:
c复制#include <stdio.h>
void bad_function(int *ptr) {
*ptr = 42; // ptr为空指针,这里会崩溃
}
int main() {
int *p = NULL;
bad_function(p);
printf("done\n");
return 0;
}
编译并进入gdb:
bash复制gcc segfault.c -o segfault -g -Wall
gdb ./segfault
执行run,输出类似:
code复制Program received signal SIGSEGV, Segmentation fault.
0x00000000004005bc in bad_function (ptr=0x0) at segfault.c:5
信息量很大:SIGSEGV说明是段错误,bad_function (ptr=0x0)说明gdb已经知道函数参数是空指针,segfault.c:5指出崩在第5行。再执行backtrace:
code复制(gdb) backtrace
#0 bad_function (ptr=0x0) at segfault.c:5
#1 0x00000000004005d0 in main () at segfault.c:11
调用链一目了然:main函数第11行调用bad_function,bad_function第5行崩溃。用info locals查看当前函数局部变量,print *ptr还能看到解引用空指针前ptr的值。整个排查过程不需要加任何printf,源码原样就能定位问题。
5.4 当程序崩溃时不再直接退出:core文件与事后分析
线上环境不能挂着gdb调试时,可以开启core文件功能。程序崩溃时操作系统会把内存镜像写到core文件里,事后再用gdb加载分析。
先检查并打开core文件开关:
bash复制ulimit -c unlimited
这条命令把core文件大小限制设为无限制。再运行崩溃程序,当前目录会生成一个core或core.进程号的文件。事后分析:
bash复制gdb ./segfault core
进入gdb后直接backtrace,能看到程序崩溃时的调用栈和前面实时调试完全一致。这个技术对排查上线后偶现崩溃非常有用——让现场保留下来,事后慢慢分析。
如果core文件没有生成,先检查ulimit -c是否显示unlimited,再看/proc/sys/kernel/core_pattern指向的路径。有些系统默认把core文件统一丢到了指定目录,不一定在当前目录下。
6. 四件套协同:从零搭建一个C/C++开发环境的完整流程
单独讲完四个工具后,我把它们串起来走一遍完整流程,你就知道这套工具链是怎么配合的了。
在全新的CentOS或Rocky Linux上,你可能连gcc都没有。先装编译器:
bash复制yum install -y vim gcc gcc-c++ gdb
gcc-c++这个包名容易踩坑,有人只装了gcc就写C++代码,编译时发现g++命令不存在,还得回头补装。顺手把vim和gdb一起装了,后面写代码、调试就都齐了。
写第一个程序:
bash复制vim hello.c
输入代码:
c复制#include <stdio.h>
int main() {
printf("Hello, Linux toolchain\n");
return 0;
}
:wq保存退出。编译:
bash复制gcc hello.c -o hello -Wall -g
运行:
bash复制./hello
调试:
bash复制gdb ./hello
(gdb) break main
(gdb) run
(gdb) next
(gdb) print 变量名
(gdb) quit
这套流程走顺之后,你Linux下开发的基本功就真正立住了。
这里我想特别强调一件事:先用这条命令链解决依赖和安装,再考虑图形化工具。很多新手在Linux下第一反应是找"IDE",然后花大量时间折腾VS Code的远程连接、插件配置。不是说IDE不好,而是你还没建立对底层工具链的感知就跳到上层,出了问题会非常被动——你不知道是编译器的问题还是IDE配置的问题。把vim + gcc + gdb这套底层的跑通,你再用任何图形化工具,心智模型都是清楚的。
7. 高频踩坑清单:把这些错误先记住,能省半天时间
把我在实际使用中遇到最多的问题汇总了一下,按工具分类整理成表格,遇到类似报错可以直接对照。
| 报错信息 | 原因分析 | 解决方向 |
|---|---|---|
If yum is not available, try dnf |
系统版本较新,yum被dnf替代 | CentOS 8及以上用dnf系列命令 |
Error: Nothing to do |
包已安装或包名不匹配 | yum list installed | grep 包名确认 |
Another app is currently holding the yum lock |
上一次yum进程未正常退出 | rm -f /var/run/yum.pid后重试 |
E45: 'readonly' option is set |
文件为只读权限 | :w!强制写入或sudo vim |
Can't open file for writing |
无写入权限 | 确认当前用户对文件所在目录有写权限 |
undefined reference to 'xxx' |
链接阶段找不到实现 | 检查是否漏加了-l库参数或源文件 |
yum锁的问题值得多说一句:如果之前强制中断了yum进程,锁文件可能残留。直接删pid文件前,用ps -ef | grep yum先确认没有yum进程还在跑,否则可能搞坏rpm数据库。
undefined reference错误是最容易让新手懵的。你明明调用了某个函数,编译阶段全过了,链接阶段报错。这通常不是什么大问题,只是编译器不知道这个函数的实现在哪。要么是忘记链接对应的库,要么是源文件没有添加到编译命令里。检查顺序:先看是不是自己写的函数但没把那个.c文件编进来,再看是不是系统库函数但没加-l参数。
gdb退出时提示quit anyway? 是因为当前有断点或进程还在运行状态,再输入一次quit确认即可。有时候直接输入y回车更快。
还有一个vim相关的常见疑问:为什么输入方向键会变成ABCD?这是因为终端传输的是转义序列,vim在某些终端模式下没有正确识别。在.vimrc里加上set nocompatible通常能解决。
8. 扩展思考:这套工具链在嵌入式与服务器场景下的特殊之处
如果你后续走嵌入式方向(比如开发板上跑Linux),会发现这套工具链会以另一种形态出现。交叉编译器的名字变成arm-linux-gnueabihf-gcc,gdb变成arm-linux-gnueabihf-gdb。命令用法完全一致,但背后多了一层"目标平台"的概念——你是在x86的主机上编译出ARM架构的程序,然后在开发板上运行。yum在嵌入式设备上则可能被opkg或apt替代,这取决于开发板的发行版。
嵌入式调试最常见的两个报错,一个是"Could not connect to target",一个是"gdb server quit unexpectedly"。前者通常是调试器(如J-Link)和目标芯片之间的连接问题,检查接线、供电、调试接口是不是被占用;后者要看gdb server的输出日志,常见原因是目标板供电不稳定或者固件跑飞。在服务器生产环境,gdb的core文件分析则是排查服务崩溃的主力手段——服务崩了,进程管理器把它拉起来,启动参数里加上--core-file让崩溃时留下现场,然后用gdb慢慢分析。
这套工具链的底层逻辑在哪个场景都是一样的:装依赖、写代码、编译验证、定位问题。工具变了,方法论不变。
我自己的体会是,Linux工具链的学习曲线虽然陡,但它每个设计都有明确的目的。vim模式切换看似别扭,实际是在避免编辑器状态混乱;gcc的阶段拆分看似繁琐,实际上让每次报错都能定位到具体环节;gdb不是替代printf,而是在printf搞不定的时候接棒。把这些"为什么"想通了,工具就变成你的能力而不是负担。
