GCC 和 GDB命令简单解读

最近在带一个刚接触 Linux 的朋友过 C 的开发流程,发现编译和调试这两个环节,看着简单,真上手的时候一堆小细节会把人卡住。趁这个机会把我自己踩过的坑整理一下,也当是给自己留个备忘。

编译到底发生了什么

很多人写 C 就是 gcc test.c -o test 一条命令跑完,能出结果就完事了。但一旦中间出错,或者要分步看中间产物,就得搞清楚这条命令背后其实是四步。

我习惯把这四步对应到四个后缀,记起来不容易混:

  • 预处理(-E):把 #include 的头文件展开、宏替换掉,输出 .i
  • 编译(-S):把 C 源码翻译成汇编,输出 .s
  • 汇编(-c):把汇编转成机器码的目标文件,输出 .o
  • 链接:把目标文件和库拼成可执行文件

这里有个特别容易栽跟头的地方:-S 是大写,-c 是小写。我第一次用的时候就记反了,gcc -s 出来的是汇编,还纳闷半天为什么没生成目标文件。这个坑到现在我还得靠"S 对应 aSsembly(汇编)"来提醒自己。

拿一段最简单的代码试一下四个参数,感受最直观。先写个 test.c:

c 复制代码
#include <stdio.h>
int main() {
    printf("hello\n");
    return 0;
}

然后依次敲这四条,看每一步生成了什么文件:

bash 复制代码
gcc -E test.c -o test.i     # 预处理:test.i 里能看到 stdio.h 被展开成一大片
gcc -S test.c -o test.s     # 编译:test.s 是汇编,能看到 call printf
gcc -c test.c -o test.o     # 汇编:test.o 是二进制目标文件
gcc test.o -o test          # 链接:test 才是能 ./test 跑的可执行文件

跑完 ls 一下,test.i、test.s、test.o、test 四个文件都在,比背概念清楚多了。

另一个更值得较真的点是,编译阶段的输入到底是 .c 还是 .i。严格说,.c 要先经过预处理变成 .i,.i 才是编译这一步真正的输入:

复制代码
test.c  --预处理-->  test.i  --编译-->  test.s  --汇编-->  test.o  --链接-->  test

你写 gcc -S test.c 能一步出 .s,不是因为跳过了预处理,而是 gcc 在内部顺手把预处理做了。要较真的话,应该显式分两步:

bash 复制代码
gcc -E test.c -o test.i     # 预处理,得到 .i
gcc -S test.i -o test.s     # 把 .i 编译成 .s

平时我们不会真的这么分步,但搞清楚这个顺序,对理解"为什么有时候报错报在头文件里"很有帮助------因为那正是预处理阶段展开头文件时出的问题。

想验证每一步的产物对不对,可以用 file 看一眼:

bash 复制代码
file test.i     # C source, ASCII text
file test.s     # assembler source
file test.o     # ELF relocatable object file
file test       # ELF executable

一目了然,比空想管用。

一次完整的调试流程

光会单个命令没用,得知道整条流程怎么串起来。我拿前面那个求平方和的程序走一遍。假设源码 sum.c 长这样:

c 复制代码
#include <stdio.h>
int print(int num) {
    int ret = num * num;
    return ret;
}
int myfunc(int num) {
    int i = 1, sum = 0;
    while (i <= num) {
        sum += print(i);
        i++;
    }
    return sum;
}
int main() {
    int num = 0;
    scanf("%d", &num);
    int result = myfunc(num);
    printf("%d", result);
    return 0;
}

第一步,编译的时候带上 -g,这步省了后面全废:

bash 复制代码
gcc -g sum.c -o sum

第二步,进 gdb 把这个程序挂上去:

bash 复制代码
gdb sum

进去之后,(gdb) 提示符就等着你下命令了。我一般先看两眼源码,确认行号,再设断点:

bash 复制代码
(gdb) list            # 看看源码带行号
(gdb) break print     # 在 print 函数入口设断点

第三步,跑起来:

bash 复制代码
(gdb) run

程序会卡在 scanf,敲 5 回车。然后第一次调用 print(1) 时命中断点,gdb 会停下并显示类似:

复制代码
Breakpoint 1, print (num=1) at sum.c:3
3       int ret = num * num;

第四步,单步走,边看变量:

bash 复制代码
(gdb) next            # 执行完 ret = num * num 这行
(gdb) print num       # 看 num 是几
(gdb) print ret       # 看 ret 算出来是多少
(gdb) continue        # 放行,下一次循环进 print(2) 会再停

print 被调了 5 次,断点会命中 5 次,每次 continue 放行一次。到最后程序输出结果 55,gdb 提示 [Inferior 1 (process ...) exited normally]。

第五步,看完退出:

bash 复制代码
(gdb) quit

这套「-g 编译 → gdb 挂载 → list 看源码 → break 设断点 → run 跑 → next/print 单步看变量 → continue 放行 → quit 退出」的顺序,是我日常调试的标准动作,练熟了后面查 bug 都是这套路子的变形。

调试时最容易懵的两个瞬间

gdb 这东西,最吓人的不是报错,而是"没反应"。我有一次调试一个带 scanf 的程序,run 之后屏幕停在 (gdb) 提示符,我还以为程序死锁了。其实根本不是------程序跑到 scanf("%d", &num),正等着我敲数字。

这种时候直接输个数字回车就行,比如:

复制代码
(gdb) 5            # 喂给 scanf
(gdb) print num    # 看看读进来没有
(gdb) continue     # 继续往下走

拿前面那个求平方和的例子说:程序里 main 有一句 scanf("%d", &num),run 起来后光标停在那,不是卡了,是它正在等你输入。敲 5 回车,程序才继续往下跑,第一次进入 print 函数时就命中你设的断点。后来想想这事挺蠢的,但第一次遇到的人十个有八个会愣住。

另一个高频问题是:设断点的时候不知道那行代码在第几行。我在没有行号的场景下会优先用函数名设断点,直接绕开行号:

复制代码
(gdb) break print     # 在 print 函数入口停
(gdb) break main      # 在 main 入口停

如果确实想按行号来,list 命令能直接列出带行号的源码,看清楚了再 break 行号:

复制代码
(gdb) list            # 列出当前函数的代码
(gdb) list 1,50       # 列出第 1 到 50 行

比如 list 出来长这样:

复制代码
1    #include <stdio.h>
2    int print(int num){
3        int ret = num * num;
4        return ret;
5    }

一看就知道 print 那行在第 3 行,直接 break 3 就行。

不过所有这些的前提是编译时加了 -g。忘了加 -g 的话,list 会告诉你没有符号表,源码都看不到,断点也就无从谈起。所以我自己的习惯是:只要是为了调试编的,一定加 -g,不省这一下。

常用命令记这么几个就够日常用了:

命令 缩写 干什么
next n 单步,不钻进函数
step s 单步,钻进函数
print 变量 p 变量 看变量值
continue c 跑到下一个断点
bt --- 看调用栈
quit q 退出

那个 No such file or directory

这个报错也值得单独说一句。./sum 跑不起来,报 No such file or directory,大部分人第一反应是"文件没了",其实更多时候是根本没编译出 sum 这个可执行文件。

排查就三步:先 ls -l 看目录里到底有什么,再 gcc sum.c -o sum 编译,最后 ./sum 跑。特别要注意的是,gcc sum.c 不带 -o 的话,默认产物叫 a.out,这时候你跑 ./sum 当然找不到------文件压根不叫这名。

举个最典型的翻车现场:你以为已经编译好了,直接 ./sum,结果:

复制代码
$ ./sum
-bash: ./sum: No such file or directory

跑个 ls 一看,目录里只有 sum.c,压根没有 sum。要么是忘了编译,要么是 gcc sum.c 没加 -o sum,产物叫 a.out 去了。补一句 gcc sum.c -o sum,再 ./sum 就通了。


编译和调试这块,说穿了就是几个反直觉的细节:-S/-c 的大小写、.i 和 .c 谁是编译的真输入、程序"卡住"其实是在等输入、忘了 -g 就看不到源码。这些坑踩过一次就记住了,写下来希望有人能少踩几个。

相关推荐
省钱兄--zs1 小时前
西安24小时自助健身房解决方案实战:系统开发与部署全流程指南
java·spring boot·系统架构·intellij-idea·需求分析
H.莓飛1 小时前
【数据结构】二叉树_OJ题
linux·开发语言·数据结构·算法
资料库011 小时前
Linux 8个常见运维故障排查
java·linux·运维
要开心吖ZSH2 小时前
MySQL 慢 SQL 排查操作手册-个人笔记版
java·笔记·sql·mysql·慢查询
u0111026752 小时前
图片处理接口如何统一错误信息 前端提示与后端错误码设计
前端·状态模式
福兮说2 小时前
浏览器里把 iPhone 的 HEIC 转成 JPG:Chrome 解不开、转完大了七成、拍摄时间全丢,六个坑实测
前端·javascript·图像处理·chrome·ios·iphone·heic
零基础1232 小时前
Ubuntu 常用命令汇总
linux·运维·开发语言
libokaifa2 小时前
把语音模型塞进手机:sherpa-onnx 在 Android 上的 ASR / TTS
前端
Android系统攻城狮3 小时前
Linux Gstreamer深度解析之gst_audio_sink_get_type调用流程与实战(六十五)
linux·运维·服务器·音视频·gstreamer音视频·音视频进阶·gstreamer音视频进阶