
1. 一个hello world到底经历了什么先看全流程地图我最早学C语言的时候写了一个hello world然后老师告诉我在命令行敲gcc hello.c -o hello回车程序就出来了。当时觉得这事儿没什么大不了直到后来自己折腾Linux下的C项目、交叉编译、链接库才发现那一个回车背后藏着一整套完整流水线。如果你现在问我从源码到可执行程序中间到底发生了什么我会告诉你至少拆成四道工序预处理Preprocessing、编译Compilation、汇编Assembly、链接Linking。这四道工序在GCC里分别对应了cppC预处理器、cc1C编译器、as汇编器、ld链接器。gcc hello.c -o hello这句话只是把四道工序一次性串起来执行了真正干活的其实是后面这四个工具。有个特别直观的验证方法执行gcc -v hello.c -o helloGCC会把每一步实际运行的命令都打印出来。我第一次看到输出里那一串路径和参数才意识到原来gcc只是一个前端调度器。你可以看到它先调用cc1把hello.c编译成临时汇编文件再调用as汇编成目标文件最后调用collect2一个链接器的包装把目标文件连同启动代码、C运行时库链接成可执行程序。如果想手动走一遍这四步可以用这些命令# 预处理展开头文件和宏生成纯C文件 gcc -E hello.c -o hello.i # 编译把C代码转成汇编代码 gcc -S hello.i -o hello.s # 汇编把汇编代码转成机器码目标文件 gcc -c hello.s -o hello.o # 链接把目标文件和库文件链接成可执行程序 gcc hello.o -o hello每一步留一个中间文件你就能清清楚楚看到代码一步步变形的过程。这篇文章我就按照这个顺序把这四道工序掰开揉碎讲清楚顺便带你看看那些常见编译链接报错背后的真实原因。适合看这篇文章的读者有两类一类是刚学完C语言基础语法、想搞清楚代码到底是怎么变成程序的入门者另一类是已经在写C项目但经常被undefined reference、multiple definition这些链接错误折磨想系统补上这块知识的人。2. 预处理阶段不只是文本替换那么简单2.1 头文件展开和宏替换的实际效果很多人觉得预处理就是文本替换听起来很简单但实际展开出来是非常壮观的。你写了一个两行的hello world#include stdio.h int main() { printf(hello world\n); return 0; }执行gcc -E hello.c -o hello.i之后生成的hello.i文件少说几百行多的能达到上千行。因为stdio.h里面还包含了其他头文件比如stdlib.h、string.h等每个头文件里还有条件嵌套的其它头文件全部递归展开后就成了一个巨型文件。这里有一个初学者经常困惑的问题头文件里的#ifndef和#define到底有什么用展开过程会告诉你答案。如果没有这些头文件保护符同一个头文件被两个不同头文件同时包含时里面的结构体定义和函数声明就会被重复复制两份到编译阶段就会产生重复定义的错误。预处理阶段虽然只是文本替换但它必须要配合这些保护符才能保证展开后的文件是合法、无冲突的。预处理阶段实际做的事我整理一下大概是这些删除所有注释注释对编译器没有意义处理#include指令把对应头文件内容插入到当前位置处理#define指令执行宏替换处理#ifdef、#ifndef、#if等条件编译指令处理#pragma指令给编译器传递特殊控制信息生成行号标记方便编译报错时定位到原始文件的对应行关于宏替换有个经典坑特别值得说一下。看这个代码#define SQUARE(x) x * x int a SQUARE(3 2); // 你以为的结果是25实际结果是11为什么是11因为预处理是纯文本替换替换后的代码是int a 3 2 * 3 2按照运算符优先级结果就是3 (2*3) 2 11。这就是为什么写带参宏一定写成#define SQUARE(x) ((x) * (x))。这种坑几乎每个写C的人都踩过理解了预处理是文本替换这一点你就永远不会再犯这个错。2.2 条件编译一个文件适配多种环境的核心预处理阶段的另一个重要职责是条件编译。嵌入式开发中经常需要一套代码适配不同型号的单片机或者一套代码同时支持Linux和Windows平台靠的就是#ifdef这种预处理指令。#ifdef _WIN32 #include windows.h #define CLEAR_SCREEN() system(cls) #else #include stdlib.h #define CLEAR_SCREEN() system(clear) #endif这里的关键点在于条件编译是在预处理阶段完成的不是在程序运行时。也就是说编译器只会保留#ifdef条件为真的那部分代码去编译另一部分代码在进入编译器之前就被删掉了。这样程序体积更小也不会出现因为平台差异导致的编译错误。日常编译时我们经常用-D参数定义宏来切换编译条件# 定义DEBUG宏让程序带上调试信息 gcc -DDEBUG main.c -o main # 代码里这样写 #ifdef DEBUG printf(debug info: %s:%d\n, __FILE__, __LINE__); #endif这种手法在大型项目里非常常见尤其是Linux内核源码里几乎到处都能看到#ifdef CONFIG_XXX这样的代码。CONFIG_XXX这些配置宏就是在编译内核之前的配置阶段生成到头文件里的。理解了预处理阶段的条件编译机制再看内核源码或者一些开源库里的平台兼容代码你就不会觉得眼花缭乱了。2.3 预处理阶段的实用技巧我觉得有一个技巧特别实用用预处理来排查宏定义冲突。有时候一个项目里定义了某个宏但这个宏在头文件里恰好也被定义了导致代码行为异常。这时候直接看预处理后的文件就能立刻定位问题。# 查看预处理后的文件中特定宏被展开成什么 gcc -E main.c -dM | grep 宏名-dM选项让GCC在预处理结束后把所有宏定义都打印出来配合grep可以快速确认当前编译环境下某个宏的值。在排查跨平台编译问题时这个方法能节省大量时间。3. 从C代码到汇编代码编译器内部在干什么预处理之后是真正的编译阶段也就是把hello.i变成hello.s。这一步是整个流程里最复杂、最有含金量的阶段也是编译原理这门学科的核心研究对象。这一阶段做的事情按顺序是词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成。不要觉得这些术语离你很远我一个个拆开讲。3.1 词法分析和语法分析编译器怎么读懂你的代码词法分析就像是把一句话拆成一个个单词和标点符号。编译器把源代码从头到尾扫描一遍把int、main、(、)、{、}、数字、字符串等切分成一个一个记号Token。如果源代码里有个不认识的字符比如中文状态下误输入了一个全角符号词法分析阶段就会报出类似unknown character的错误。语法分析则是这些Token按照C语言的语法规则组合成句子。比如int main()这个结构语法分析器会根据C语言的文法规则判断这是类型标识符参数列表函数体的合法函数定义。如果漏写了一个分号或者括号不匹配在这里就会报语法错误。有一件事必须说清楚编译器在语法和语义分析阶段根本不会执行你的代码它只关心代码是否符合C语言标准。这就像语法老师批改英语作文只看句子结构对不对不会关心你写的内容符不符合事实。逻辑错误编译器是抓不到的哪怕你的代码运行结果完全错误只要语法正确编译器也会让它通过。3.2 中间代码生成与优化编译器的深思熟虑语法分析通过后编译器不会直接生成汇编代码而是先生成一种中间表示Intermediate Representation简称IR然后在这个中间表示上做大量优化。之所以要这样设计是因为直接在源代码层面做优化很困难而中间表示是一种接近机器语言的形态既保留了程序逻辑又方便做各种变换。编译器在优化阶段会干这些事常量折叠int a 3 * 4;直接算成int a 12;死代码消除如果一段代码的执行结果不会被用到直接删掉循环优化把循环里的不变计算移到循环外内联展开把短函数的函数体直接插入到调用处减少函数调用开销GCC默认使用-O0不优化到-O3激进优化这几个优化等级。调试时用-O0发布时用-O2这是最常见的选择。我之前遇到过一个问题一个函数在-O0下运行正常-O1下就出错了。后来查出来是代码里有一个未初始化变量严格来说这是未定义行为编译器在优化时利用了未定义行为不会发生这个假设把代码路径改变了。这个教训就是写代码时一定要处理所有未定义行为否则在低优化等级下侥幸能跑高优化等级下就会暴雷。如果想看编译器优化后的汇编代码长什么样可以用gcc -S -O2 hello.c -o hello.s。初学汇编的人可能会惊讶自己写的十行C代码优化后汇编只剩寥寥几行因为很多操作被编译器理解后合并简化了。3.3 汇编代码生成与目标文件从人能读到机器能读优化完成后编译器把中间表示转换成汇编代码然后交给汇编器处理。汇编器的工作相对机械把每条汇编指令翻译成对应的机器指令生成目标文件.o文件。目标文件虽然已经是二进制格式但它还不是可执行程序因为里面还有很多未解决的引用。gcc -c hello.c -o hello.o生成的目标文件可以用工具查看它的内部结构# 查看目标文件的头部信息和各个段section readelf -h hello.o # 查看符号表 nm hello.o你会看到目标文件里通常有几个段.text代码段存放机器指令.data数据段存放已初始化的全局变量和静态变量.bss存放未初始化的全局变量和静态变量不占文件空间运行时分配.rodata存放只读数据比如字符串常量用nm hello.o查看符号表你可能会看到类似这样的输出0000000000000000 T main U printfT main表示main函数在这个目标文件里有定义T代表text段U printf表示printf是一个未定义符号U代表undefined意思是这个目标文件引用了printf但printf的实现不在这个文件里。这些未定义符号就是链接阶段要解决的问题。这也是理解编译和链接区别的一个核心视角——编译阶段只关心单个源文件内部的语法语义是否正确而一个源文件里调用了外部函数或者引用了一个外部全局变量这些跨文件的关系是在链接阶段才建立起来的。4. 目标代码与启动过程编译器和库之间有条隐秘边界我们已经知道编译阶段会生成目标文件但你可能没有认真想过一个问题你写的main函数真的是程序运行的起点吗不是。程序真正的入口其实是一个叫_start的启动函数它不在你的源码里而是在C运行时库C Runtime简称CRT的启动代码里。链接器在链接时会把启动代码的目标文件通常是crt1.o和你的目标文件链接在一起。启动代码的职责大致是从操作系统拿到程序的运行时环境初始化全局变量和静态变量初始化C标准库的运行时环境准备好argc和argv参数调用main函数main返回后把返回值作为进程退出码传给操作系统的退出系统调用所以_start函数的大致逻辑就是void _start() { // 初始化各种运行时环境 ... // 调用main函数 int ret main(argc, argv); // 退出 exit(ret); }这就是为什么链接阶段需要把目标文件和特殊的启动文件、库文件链接起来——没有这些你的main就没有人帮你调用程序也不知道怎么启动和退出。关于这一点有一个直观的验证方式单独用ld链接一个简单的目标文件不链接启动代码和标准库看看会报什么错。你会看到一堆undefined reference to _start或者类似错误。这就是因为链接器找不到程序的总入口。对于嵌入式开发这个边界尤其重要。在裸机环境下没有操作系统_start通常就是一个简单的启动汇编文件负责设置栈指针、初始化数据段、跳转到main。比如在ARM嵌入式开发里经常要自己写一个startup.s汇编启动文件这和PC上使用的标准启动流程虽然细节不同但逻辑框架是一致的。还有一种情况是把main改名为其他名字# 让编译器不链接标准启动文件入口函数改为my_entry gcc -nostartfiles -e my_entry main.c -o app-e选项可以指定程序的入口符号。这种方式在写操作系统内核、嵌入式裸机代码时很常用因为内核的入口不是普通意义上的main函数而是一个初始化汇编入口点。5. 链接阶段把碎片拼成完整程序的艺术5.1 链接器到底在解决什么问题编译阶段每个源文件都是独立编译的互不干扰。但一个项目通常有成百上千个源文件这些文件之间会互相调用函数、共享全局变量。链接器要解决的核心问题有两个符号解析Symbol Resolution和重定位Relocation。符号解析就是每个目标文件里都有一些未定义的符号比如你调用了printf但没有定义它链接器要在其他目标文件或者库文件里找到这些符号的定义把引用和定义对应起来。重定位则是当多个目标文件链接在一起时每个文件里的代码段和数据段都要放到最终可执行文件的地址空间里。但单个目标文件在编译时并不知道自己最终会被放在哪个地址所以里面的地址引用都是相对的、临时的。链接器需要根据最终的布局修改这些引用让它们指向正确的地址。用一个简单的例子来说假设有两个文件// a.c extern int add(int x, int y); int main() { return add(3, 4); } // b.c int add(int x, int y) { return x y; }编译后a.o里有一个对add的未定义引用b.o里有add的定义。链接器把这两个文件合并时会做两件事在符号表里找到add的定义让a.o里的调用指向b.o里add函数的地址如果b.o里的add函数被放在最终布局的某个地址链接器要把a.o里调用指令的地址字段改成这个最终地址你可以用objdump -d查看链接前后可执行文件的机器码差异会看到链接前调用指令后面的地址是0或者占位符链接后变成了真实的函数地址。5.2 静态链接把库代码直接打包进可执行文件静态链接是链接器把程序和它依赖的库函数代码直接合并到最终的可执行文件里。比如你的程序调用了printf静态链接后printf的实现代码就被复制进你的可执行文件里了。静态链接的可执行文件有什么特点优点运行时不需要依赖外部库可移植性强拷到另一台同架构机器上就能直接跑缺点文件体积大多个程序都静态链接同一个库时磁盘和内存里会有很多份重复的库代码静态链接库实际上就是一个.a文件它是由若干.o文件打包而成的归档文件。我们可以用ar命令创建# 把b.o打包成静态库libb.a ar rcs libb.a b.o # 链接时告诉编译器使用这个库 gcc a.o -L. -lb -o app这里的-L.表示在当前目录下找库-lb表示找名为libb.a或libb.so的库文件。这是C语言新手很容易踩坑的地方库文件命名必须遵循lib前缀加名字加.a或.so后缀的约定链接器才会自动搜索。5.3 动态链接加载时再绑定动态链接则完全不同链接器在链接阶段只做一次登记记录下可执行文件依赖哪些动态库但并不把库的代码复制进来真正的绑定延迟到程序运行时。动态链接的可执行文件会在.dynamic段里记录依赖的库名比如用readelf -d app可以看到类似NEEDED libc.so.6这样的信息。程序启动时由动态链接器也叫动态加载器负责找到这些库、加载到内存、完成符号绑定。动态链接的优势是可执行文件体积小库代码在内存中可以共享多个进程使用同一个动态库时内存只保存一份库更新时只要保持接口兼容不需要重新编译可执行文件日常Linux环境下libc.so.6就是C标准库的动态链接版本几乎所有的C程序都依赖它。用ldd app命令可以查看一个可执行程序依赖了哪些动态库以及每个库的解析路径。5.4 静态链接和动态链接的选择没有绝对好坏我见过很多刚入门的开发者要么执着于全部静态链接要么在部署时才发现动态库缺失。其实选择哪种方式要看具体的应用场景。我习惯用一个表格来对比对比项静态链接动态链接可执行文件体积大小运行时依赖无依赖特定动态库内存占用高每个进程各自持有一份低库代码共享更新维护需重新编译链接替换库文件即可启动速度稍快无需加载动态库稍慢运行时加载和解析部署难度简单需要确保目标系统有对应的库举个例子如果写的是一个内部使用的小工具用静态链接最省心拷到哪都能跑。如果写的是一个运行在服务器上的服务程序用动态链接更合适因为glibc等基础库几乎在所有Linux发行版上都有而且升级库修复安全漏洞时服务程序不需要重新编译。实际编译时可以通过-static强制动态链接器的静态模式# 全静态链接 gcc -static hello.c -o hello_static # 查看静态版本和动态版本的大小差异 ls -lh hello_dynamic hello_static我实测过同一个hello world动态链接版本可能只有16KB左右静态链接版本可能到700KB以上。差距还是很明显的。5.5 链接顺序的经典陷阱和符号冲突链接阶段有个很经典的问题库的链接顺序会影响链接是否成功。看这个命令gcc main.o -lfoo -lbar -o app如果libfoo.a里的某个函数引用了libbar.a里的另一个函数这个顺序通常是正确的。但如果颠倒顺序gcc main.o -lbar -lfoo -o app就有可能报undefined reference即使libfoo.a和libbar.a都确实提供了对应的符号。原因在于链接器在处理库时是一次性的从左到右扫描扫描到静态库时只提取库中被当前已经收集的未定义符号所满足的目标文件如果某个符号在当前库中未找到它不会回头再搜前面的库。解决方案有几种把库放到右侧重复写几遍gcc main.o -lfoo -lbar -lfoo -o app使用--start-group和--end-group参数让链接器多轮扫描把互相依赖的库合并成一个库同样值得警惕的还有符号冲突。当链接的多个目标文件或库里都有同名函数时链接器可能报multiple definition错误。比如你在两个源文件里都定义了名为helper的函数这两个文件又都参与了链接链接器就会报错。这个错误的本质是C语言要求全局符号函数名、全局变量名在同一个程序里是唯一的。6. 动态链接器到底在做什么程序启动时的后置绑定6.1 装载、搜索路径与全局偏移表的角色动态链接机制下程序要真正跑起来必须先靠动态链接器把所有需要的动态库加载进来。Linux下的动态链接器通常是/lib64/ld-linux-x86-64.so.2。动态链接器在程序启动时完成的主要工作读取可执行文件里记录的依赖库列表按顺序搜索动态库的搜索路径找到每个库把库映射到进程的地址空间解析和重定位库里的引用关系把控制权交给程序的_start入口动态库搜索路径的顺序是很多人会遇到问题的地方。在Linux下动态链接器的搜索顺序大致是1. 环境变量LD_LIBRARY_PATH指定的路径 2. 缓存文件/etc/ld.so.cache中的路径由ldconfig更新 3. 系统默认路径如/lib、/usr/lib如果你把自定义的动态库放在某个目录下但没有告诉动态链接器运行程序时就会看到./app: error while loading shared libraries: libmyfoo.so: cannot open shared object file: No such file or directory这个错误并不是程序本身的问题而是动态链接器找不到对应的库。解决办法可以是# 方法一设置环境变量临时生效 export LD_LIBRARY_PATH/path/to/your/lib:$LD_LIBRARY_PATH ./app # 方法二把库安装到系统默认路径 sudo cp libmyfoo.so /usr/local/lib/ sudo ldconfig # 方法三把自定义路径加入ld.so.conf后刷新缓存 echo /path/to/your/lib | sudo tee /etc/ld.so.conf.d/myapp.conf sudo ldconfig其中ldconfig命令刷新动态链接器缓存生成.so文件的SONAME关联关系是Linux系统维护动态链接的核心工具。我在自己项目中经常用的ldd app命令就是用来检查一个程序依赖了哪些库、这些库是否能被正常找到、最终解析到哪个路径的。6.2 动态链接的延迟绑定机制第一次调用才解析动态链接还有个有意思的机制叫延迟绑定Lazy Binding。早期版本的动态链接器会在程序启动时就解析所有动态库里的函数引用这会拖慢启动速度。现代动态链接器比如glibc的实现默认采用延迟绑定只有当程序第一次调用某个动态库函数时才去解析这个函数的真实地址。这个机制的具体实现涉及两个数据结构GOT全局偏移表和PLT过程链接表。简单来说程序调用一个动态库函数时并不是直接调用库里的函数地址而是跳转到PLT中对应的一个桩函数第一次执行时会触发动态链接器解析真实地址并写入GOT后续调用就直接从GOT里取地址了。这背后有一个值得注意的问题如果你的动态库符号解析有问题程序可能启动时不报错直到某个动态库函数被第一次调用时才崩溃或报错。排查这种问题时用LD_DEBUGall ./app可以打印出动态链接器每一步的操作细节能看到每个符号在什么时候被解析。这个命令我第一次用的时候被输出的信息量吓到了但定位问题是真的好用。7. 编译和链接中常见问题的排查思路7.1 编译错误和链接错误的本质区别把编译和链接分清楚后很多报错你一眼就能看出问题在哪。我总结一个基本的区分方法编译错误报错信息里带有源文件名和行号通常是xxx.c:10:5: error: ...这样的格式错误内容一般是语法错误、类型不匹配、变量未声明等链接错误报错信息里带有undefined reference、multiple definition这类关键词不涉及具体行号通常只给出符号名编译错误是你的源代码不符合C语言规则链接错误是你的源代码符合规则但组成程序的所有零件之间对不上号。7.2 三类最常见的链接错误排查链路第一类undefined reference符号未定义看到这个错误排查链条一般是确认报错的符号名是什么用nm在目标文件里查到不到这个符号的定义确认是否缺少对应的源文件没编进去或者库没链接进来确认库的链接顺序是否正确确认符号名是否因为C和C混编变了C有名字修饰机制需要用extern C如果是三方库的问题可以用nm -D libxxx.so | grep 符号名确认库中是否真的导出了这个符号。第二类multiple definition重复定义这个错误的意思是多个目标文件里都定义了同一个全局符号。常见原因两个源文件写了同一个全局函数名头文件里定义了全局变量应该只用extern声明在某个源文件里定义项目里误链接了同一个库的两个版本排查方法比较简单用nm查看冲突符号在哪些目标文件里出现找到重复的来源删掉多余的那份定义。第三类无法找到动态库cannot open shared object file这个错误我在部署时遇到过很多次主要是程序开发环境正常运行但换了一台机器后自定义的动态库没有安装过去。排查顺序是用ldd app看缺哪个库确认这个库是否在新机器上存在如果存在确认路径是否在动态链接器的搜索路径里用LD_LIBRARY_PATH或ldconfig配置好路径7.3 交叉编译环境下的链接问题补充热词里出现了busybox编译arm、嵌入式内核源码我顺便说一下交叉编译场景。交叉编译时工具链用的是一个针对目标平台比如ARM的编译器而不是本机的编译器。链接器也需要是交叉工具链里的链接器比如arm-linux-gnueabihf-gcc对应的链接器是arm-linux-gnueabihf-ld。交叉编译中最容易踩的坑是库路径配置错误。如果你在x86主机上编译ARM目标程序但误用了本机的/usr/lib下的库链接器会报架构不匹配的错或者更隐蔽地链接了错误架构的库导致程序运行时直接崩溃。牢记交叉编译时库路径要指向目标平台的sysroot目录不能使用本机默认路径。还有一个常见问题是交叉编译工具链版本和目标板上的库版本不匹配。用较老工具链编译的程序可能在较新系统上也能跑但反向通常会有兼容性问题。嵌入式开发中一个稳妥的做法是保持编译环境、目标环境、库版本三者版本一致。8. 实战案例用一个多文件项目走完整个链接流程8.1 一个简单的两文件加一库项目为了让你对全流程有直观印象我带你完整走一个项目。假设我们要写一个小程序main.c里调用一个数学工具函数add和一个打印函数show_result其中add放在math_util.c里show_result放在print_util.c里。文件内容// main.c #include math_util.h #include print_util.h int main() { int result add(10, 20); show_result(result); return 0; } // math_util.h #ifndef MATH_UTIL_H #define MATH_UTIL_H int add(int a, int b); #endif // math_util.c #include math_util.h int add(int a, int b) { return a b; } // print_util.h #ifndef PRINT_UTIL_H #define PRINT_UTIL_H void show_result(int val); #endif // print_util.c #include stdio.h #include print_util.h void show_result(int val) { printf(result %d\n, val); }8.2 一步一步链接的过程和验证第一步把每个源文件编译成目标文件gcc -c main.c -o main.o gcc -c math_util.c -o math_util.o gcc -c print_util.c -o print_util.o现在用nm看看每个目标文件里的符号nm main.o输出大概是U add U show_result 0000000000000000 T main可以看到main.o引用了add和show_result但它们的定义在其他文件里。第二步把工具文件打包成静态库ar rcs libutils.a math_util.o print_util.o第三步链接成可执行程序gcc main.o -L. -lutils -o app运行./app输出result 30。这个过程看起来很简单但如果你想深入理解链接器实际干了什么可以用这个命令看细节# 查看符号表里最终的地址 nm app | grep -E main|add|show_result # 反汇编add函数看ret指令之前的内容 objdump -d app | grep -A20 add你会在反汇编里看到add函数确实被加载到了最终可执行程序的某个地址main函数里调用add的地方机器码里已经填入了这个真实地址。8.3 一个实际会遇到的链接报错模拟我们模拟一个报错场景链接时漏掉了其中一个库函数实现。如果把print_util.o从库中排除掉ar rcs libutils_partial.a math_util.o gcc main.o -L. -lutils_partial -o app你会看到典型的undefined reference错误/usr/bin/ld: main.o: in function main: main.c:(.text0x14): undefined reference to show_result collect2: error: ld returned 1 exit status这个报错的排查思路就是按照我在前面写的确认符号名确认哪个目标文件/库里应该有定义确认链接命令是否正确包含了它。很多时候不是代码有问题而是链接命令漏了文件或者库。9. 编译工具链里那些容易被忽视的实用工具除了gcc和ld工具链里还有几个日常排查问题非常有用的命令。每个用C语言的人都应该熟练掌握。objdump反汇编和查看目标文件详情的瑞士军刀。# 反汇编可执行文件 objdump -d app # 查看目标文件的所有段 objdump -h app # 查看动态依赖 objdump -p app | grep NEEDEDreadelf查看ELF文件的详细信息比objdump更底层。# 查看ELF头部 readelf -h app # 查看符号表 readelf -s app # 查看动态段信息 readelf -d appnm列出符号表定位未定义引用最直接。nm app nm -D libc.so.6 | grep printfldd查看动态库依赖关系。ldd appstrip去掉符号表减小可执行文件体积通常在发布时使用。strip app其中strip是我在嵌入式和发布场景下很常用的一个命令。一个包含了调试符号的程序动辄几十MBstrip之后可能只有几MB。但要注意strip之后gdb调试就没法用符号信息了所以一般流程是编译时保留符号做调试发布时strip再分发。这套工具组合用下来从编译链接到二进制分析基本都能覆盖。如果你以后走的是嵌入式、Linux内核或者系统编程方向这些命令的使用频率会非常高。建议你手头有一个项目时把这些命令都跑一遍熟悉每个命令的输出格式。技术这东西看得再多都不如自己动手敲一遍来得实在。