这个命令用于查看ELF格式的可执行文件/库的符号表信息。让我详细解释:
命令解析
nm -S -n vela.elf各选项含义:
- nm- GNU二进制文件工具,用于显示目标文件的符号表
- -S- 显示符号大小(size)
- -n- 按地址/值排序(数字顺序)
- vela.elf- 目标ELF文件(通常是嵌入式或Linux可执行文件)
输出示例
0000000000401000 000000000000002a T _start 0000000000401030 0000000000000015 T main 0000000000401048 0000000000000032 T function1各列含义:
- 第一列:符号地址(16进制)
- 第二列:符号大小(字节,-S选项显示)
- 第三列:符号类型
- 第四列:符号名称
常见符号类型:
| 类型 | 含义 |
|---|---|
| T/t | 代码段中的函数/文本符号 |
| D/d | 已初始化数据段 |
| B/b | 未初始化数据段(BSS) |
| R/r | 只读数据段 |
| U | 未定义符号(需要链接) |
| A | 绝对地址符号 |
实际应用场景:
- 固件分析- 查看嵌入式系统符号布局
- 调试- 定位函数地址用于调试
- 大小分析- 查看函数/变量占用空间
- 逆向工程- 理解程序结构
相关有用命令:
# 更详细的符号信息nm -S -n --size-sort vela.elf# 显示动态符号nm -D vela.elf# 显示所有符号(包括调试符号)nm -a vela.elf# 配合objdump查看反汇编objdump -d vela.elf|grep<function_address>这个命令在嵌入式开发、内核调试和逆向工程中非常有用。
nm显示的符号主要包含全局符号,不包括局部变量和堆变量。让我详细解释:
包含的符号(会出现在nm输出中):
1.全局函数
// 会出现在nm输出voidglobal_function(){...}2.全局变量
// 会出现在nm输出intglobal_var=10;staticintstatic_global_var=20;3.静态变量(包括函数内的静态变量)
voidfunc(){staticintstatic_local_var=30;// 会出现在nm输出!// 注意:static_local_var虽然作用域是函数内,但生命周期是全局的}4.弱符号
__attribute__((weak))intweak_symbol=5;// 会出现在nm输出不包含的符号(不会出现在nm输出中):
1.局部变量(栈变量)
voidfunc(){intstack_var=100;// 不会出现在nm输出 - 栈分配charbuffer[128];// 不会出现在nm输出 - 栈分配}2.堆变量(动态分配)
int*heap_var=malloc(100);// 不会出现在nm输出 - 运行时分配3.寄存器变量
registerintreg_var;// 不会出现在nm输出4.函数参数
voidfunc(intparam){...}// param不会出现在nm输出实际验证示例:
test.c:
#include<stdlib.h>intglobal_data=42;// 会显示 - 全局变量staticintstatic_data=100;// 会显示 - 静态全局变量voidglobal_func(){// 会显示 - 全局函数staticintfunc_static=0;// 会显示 - 函数内的静态变量intlocal=5;// 不会显示 - 局部变量int*heap=malloc(10);// 不会显示 - 堆变量}intmain(){return0;}编译和查看:
gcc -c test.c -o test.o nm -S test.o可能的输出:
0000000000000000 0000000000000004 D global_data 0000000000000004 0000000000000004 d static_data 0000000000000000 0000000000000015 T global_func 0000000000000008 0000000000000004 d func_static.0 # 编译器重命名 0000000000000015 T main为什么nm不显示局部变量?
编译原理:
- 局部变量在编译时只有类型信息,没有固定地址
- 它们被编译成相对于栈指针(SP/FP)的偏移量
符号表作用:
- 主要用于链接器(需要知道哪些符号可以被其他文件引用)
- 局部变量不参与链接过程
存储位置:
- 全局/静态变量:在.data/.bss段(固定地址)
- 局部变量:在栈上(运行时确定地址)
- 堆变量:在堆上(运行时动态分配)
如何查看局部变量信息?
1.使用调试信息
gcc -g test.c -otestnm -atest|grep-ilocal# 可能包含调试符号2.使用objdump查看反汇编
objdump -d test.o# 可以看到局部变量的栈操作:# push rbp# mov rbp, rsp# sub rsp, 16 # 为局部变量分配栈空间3.使用readelf查看节区
readelf -S test.o# .text: 代码段(函数)# .data: 已初始化全局变量# .bss: 未初始化全局变量# 没有专门的"栈节区"或"堆节区"总结对比表:
| 变量类型 | nm显示 | 原因 |
|---|---|---|
| 全局函数 | ✓ | 需要链接地址 |
| 全局变量 | ✓ | 需要链接地址 |
| 静态全局变量 | ✓ | 需要分配固定存储 |
| 静态局部变量 | ✓ | 存储在.data/.bss段 |
| 局部变量(栈) | ✗ | 栈偏移,无符号表项 |
| 函数参数 | ✗ | 寄存器/栈传递 |
| 堆变量 | ✗ | 运行时动态分配 |
| 寄存器变量 | ✗ | 使用寄存器 |
关键理解:nm查看的是链接器需要知道的信息,凡是需要链接器在链接时分配地址或解决引用的符号,都会出现在符号表中。局部变量和堆变量的生命周期和作用域决定了它们不需要(也无法)在链接时处理。
为什么需要重命名?
1.作用域冲突
C语言允许在不同函数中使用相同的静态变量名:
voidfunc1(){staticintcount=0;// 需要唯一名称}voidfunc2(){staticintcount=0;// 需要唯一名称,但不能冲突}两个count变量虽然同名,但作用域不同,必须有不同的全局符号名。
2.静态局部变量的特殊性
静态局部变量具有:
- 局部作用域(只能在函数内访问)
- 全局生命周期(存储在.data/.bss段)
- 需要唯一全局符号名(用于链接器)
编译器重命名规则
GCC/Clang的典型命名模式:
<原始变量名>.<数字后缀> <函数名>.<变量名>.<数字后缀> <文件作用域前缀>.<变量名>.<数字后缀>示例分析:
// test.cvoidfunc(){staticintvar=0;// 编译为 var.0staticintcount=0;// 编译为 count.1 (同函数内递增)}voidfunc2(){staticintvar=0;// 编译为 var.2 (跨函数递增)}编译后:
$ nm test.o 0000000000000004 d var.0 0000000000000008 d count.1 000000000000000c d var.2为什么是.0而不是其他值?
数字后缀的生成规则:
同一函数内:按声明顺序从0开始递增
voidfunc(){staticinta=0;// → a.0 (第一个静态变量)staticintb=0;// → b.1 (第二个){staticintc=0;// → c.2 (第三个,即使在不同块内)}}不同函数间:编译器可能:
- 方案A:全局计数器(整个文件)
- 方案B:基于函数独立计数
- 方案C:基于编译单元独立计数
实际编译器行为(以GCC为例):
// gcc的行为示例staticintfile_static=0;// → file_static (无后缀,文件作用域已唯一)voidfoo(){staticintx=0;// → x.0staticinty=0;// → y.1}voidbar(){staticintx=0;// → x.2 (继续递增)}
查看真实的重命名
让我们实际验证一下:
test_rename.c:
#include<stdio.h>voidfunc1(){staticintcounter=0;staticinttotal=0;counter++;total+=counter;}voidfunc2(){staticintcounter=0;staticintresult=0;counter--;result=counter*2;}intmain(){return0;}编译和查看符号:
gcc -c test_rename.c -o test_rename.o nm test_rename.o可能的输出:
0000000000000000 d counter.0 # func1的第一个静态变量 0000000000000004 d total.1 # func1的第二个静态变量 0000000000000008 d counter.2 # func2的第一个静态变量 000000000000000c d result.3 # func2的第二个静态变量 0000000000000000 T func1 0000000000000015 T func2 000000000000002a T main重命名的技术实现
1.编译器的符号表处理
// 简化的编译器逻辑classCompiler{intstatic_counter=0;stringmangle_static_var(string orig_name){returnorig_name+"."+to_string(static_counter++);}};2.不同编译器的差异
| 编译器 | 命名模式 | 示例 |
|---|---|---|
| GCC | <name>.<数字> | var.0,var.1 |
| Clang | 类似GCC | var.0,var.1 |
| MSVC | ?<name>@<函数>@@<修饰> | ?var@func@@HA |
| ICC | 类似GCC | var.0_ |
3.C++的更复杂重整
C++由于命名空间、类、模板等特性,命名重整更复杂:
classTest{staticintclass_static;// → _ZN4Test11class_staticEvoidmethod(){staticintlocal;// → _ZZN4Test6methodEvE5local}};为什么不是其他命名方案?
可能的替代方案(为什么不用):
使用函数名前缀:
func_var- 优点:更易读
- 缺点:函数名可能很长,产生冗余
使用随机数:
var_0x1234- 优点:唯一性好
- 缺点:不可预测,调试困难
使用哈希值:
var_abc123- 优点:紧凑
- 缺点:可能冲突,不直观
实际选择.0的原因:
- 简单性:实现简单
- 可预测性:便于调试和反汇编
- 确定性:同一代码每次编译结果相同
- 兼容性:与调试信息协调工作
对调试的影响
1.GDB中的处理
# GDB会自动处理名称重整(gdb)info variables All defined variables: File test.c: static int func()::counter;# GDB显示原始名称static int func()::total;2.objdump查看
objdump -t test.o|grepstatic# 显示重整后的名称,但可以配合源代码理解特殊情况
1.内联函数中的静态变量
inlinevoidfunc(){staticintcount=0;// 每个编译单元独立实例}// 可能产生: count.0 (在第一个.o), count.0 (在第二个.o)2.模板中的静态变量(C++)
template<typenameT>voidfunc(){staticT counter=0;// 每个模板实例化都有独立副本}// func<int>::counter 和 func<float>::counter 是不同变量总结
为什么是.0?
- 从0开始:这是最自然的计数器起始值
- 递增策略:简单且能保证唯一性
- 点号分隔:
.不是有效的C标识符字符,避免与用户代码冲突 - 历史惯例:Unix工具链的传统做法
这种命名方案平衡了:
- 唯一性:避免符号冲突
- 可读性:便于人工识别
- 实现简单性:编译器容易生成
- 确定性:便于构建系统工作
下次你看到var.2173这样的符号时,就知道这是该编译单元中第2174个被处理的静态局部变量(如果从0开始计数)!