linux下nm命令的使⽤
什么是nm
nm命令是linux下⾃带的特定⽂件分析⼯具,⼀般⽤来检查分析⼆进制⽂件、库⽂件、可执⾏⽂件中的符号表,返回⼆进制⽂件中各段的信息。
⽬标⽂件、库⽂件、可执⾏⽂件
⾸先,提到这三种⽂件,我们不得不提的就是gcc的编译流程:预编译,编译,汇编,链接。
⽬标⽂件 :常说的⽬标⽂件是我们的程序⽂件(.c/.cpp,.h)经过预编译,编译,汇编过程⽣成的⼆进制⽂件,不经过链接过程,编译⽣成指令为:
gcc(g++) -c file.c(file.cpp)
将⽣成对应的file.o⽂件,file.o即为⼆进制⽂件
库⽂件: 分为静态库和动态库,这⾥不做过多介绍,库⽂件是由多个⼆进制⽂件打包⽽成,⽣成的.a⽂件,⽰例:
ar -rsc liba.a test1.o test2.o test3.o
将test1.o test2.o test3.o三个⽂件打包成liba.a库⽂件
可执⾏⽂件:可执⾏⽂件是由多个⼆进制⽂件或者库⽂件(由上所得,库⽂件其实是⼆进制⽂件的集合)经过链接过程⽣成的⼀个可执⾏⽂件,对应windows下的.exe⽂件,可执⾏⽂件中有且仅有⼀个main()函数(⽤户程序⼊⼝,⼀般由bootloader指定,当然也可以改),⼀般情况下,⼆进制⽂件和库⽂件中是不包含main()函数的,但是在linux下⽤户有绝对的⾃由,做⼀个包含main函数的库⽂件也是可以使⽤的,但这不属于常规操作,不作讨论。
上述三种⽂件的格式都是⼆进制⽂件。
为什么要⽤到nm
在上述提到的三种⽂件中,⽤编辑器是⽆法查看其内容的(乱码),所以当我们有这个需求(例如debug,查看内存分布的时候)去查看⼀个⼆进制⽂件⾥包含了哪些内容时,这时候就将⽤到⼀些特殊⼯具,linux下的nm命令就可以完全胜任(同时还有objdump和readelf⼯具,这⾥暂不作讨论)。
怎么使⽤nm
如果你对linux下的各种概念还算了解的话,就该知道⼀般linux下的命令都会⾃带⼀些命令参数来满⾜各种应⽤需求,了解这些参数的使⽤是使⽤命令的开始。
man
那么,如何去了解⼀个命令呢,最好的⽅法就是linux下的man命令,linux是⼀个宝库,⽽man指令就相当于这个宝库的说明书。
⽤法:
man nm
这⾥⾯介绍了nm的各种参数以及详细⽤法,如果你有⽐较不错的英⽂⽔平和理解能⼒,可以直接参考man page中的内容。
nm的常⽤命令参数
-A 或-o或 --print-file-name:打印出每个符号属于的⽂件
-a或--debug-syms:打印出所有符号,包括debug符号
-
B:BSD码显⽰
-C或--demangle[=style]:对低级符号名称进⾏解码,C++⽂件需要添加
--no-demangle:不对低级符号名称进⾏解码,默认参数
-D 或--dynamic:显⽰动态符号⽽不显⽰普通符号,⼀般⽤于动态库
-f format或--format=format:显⽰的形式,默认为bsd,可选为sysv和posix
-g或--extern-only:仅显⽰外部符号
-h或--help:国际惯例,显⽰命令的帮助信息
-n或-v或--numeric-sort:显⽰的符号以地址排序,⽽不是名称排序
-p或--no-sort:不对显⽰内容进⾏排序
-P或--portability:使⽤POSIX.2标准
-V或--version:国际管理,查看版本
-
-defined-only:仅显⽰定义的符号,这个从英⽂翻译过来可能会有偏差,故贴上原⽂:Display only defined symbols for each object file
好了,上述就是常⽤的命令参数,光说不练假把式,下⾯将给出⼀个⽰例来进⼀步理解nm⽤法:⽰例代码:
```
#include
#include
using namespace std;
const char *str="downey";
int g_uninit;
int g_val=10;
void func1()
{
int *val=new int;
linux所有命令都无法使用static int val_static=1;
cout<<"downey"<<endl;
}
void func1(char* str)
{
cout<<str<<endl;
}
```
编译指令:
g++ -c test.cpp
在当前⽬录下⽣成test.o⽬标⽂件,然后使⽤nm命令解析:
nm -n -C test.o
由于是C++源⽂件,故添加-C 选项,为了⽅便查看,添加-n选项
输出信息:
```
U __cxa_atexit
U __dso_handle
U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U operator new(unsigned long)
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, c 0000000000000000    B g_uninit
0000000000000000    D str
0000000000000000    T func1()
0000000000000004    b std::__ioinit
0000000000000008    D g_val
000000000000000c    d func1()::val_static
0000000000000035    T func1(char*)
0000000000000062    t __static_initialization_and_destruction_0(int, int)
00000000000000a0    t _GLOBAL__sub_I_str
```
下⾯我们再来解析输出信息中各部分所代表的意思吧
⾸先,前⾯那⼀串数字,指的就是地址
然后,我们发现,每⼀个条⽬前⾯还有⼀个字母,类似‘U‘,‘B‘,‘D等等,其实这些符号代表的就是当前条⽬所对应的内存所在部分
最右边的就是对应的符号内容了
⾸要的需要讲解的就是第⼆点中字符所对应的含义:
同样在还是在linux命令⾏下man nm指令可以得到:
A    :符号的值是绝对值,不会被更改
B或b  :未被初始化的全局数据,放在.bss段
D或d  :已经初始化的全局数据
G或g  :指被初始化的数据,特指small objects
I    :另⼀个符号的间接参考
N    :debugging 符号
p    :位于堆栈展开部分
R或r  :属于只读存储区
S或s  :指为初始化的全局数据,特指small objects
T或t  :代码段的数据,.test段
U    :符号未定义
W或w  :符号为弱符号,当系统有定义符号时,使⽤定义符号,当系统未定义符号且定义了弱符号时,使⽤弱符号。
:unknown符号
根据以上的规则,我们就可以来分析上述的nm显⽰结果:
⾸先,输出的上半部分对应的符号全是U,跟我们常有思路不⼀致的是,这⾥的符号未定义并不代表这个符号是⽆法解析的,⽽是⽤来告诉链接器,这个符号对应的内容在我这个⽂件只有声明,没有具体实现,如std::cout,std::string类,在链接的过程中,链接器需要
到其他的⽂件中去到它的实现,如果不到实现,链接器就会报常见的错误:undefined reference。
在接下来的三⾏中
0000000000000000 B g_uninit
0000000000000000 D str
0000000000000000 T func1()
令⼈疑惑的是,为什么他们的地址都是0,难道说mcu的0地址同时可以存三种数据?其实不是这样的,按照上⾯的符号表规
则,g_uninit属于.bss段,str属于全局数据区,⽽func1()属于代码段,这个地址其实是相对于不同数据
区的起始地址,即g_uninit 在.bss段中的地址是0,以此类推,⽽.bss段具体被映射到哪⼀段地址,这属于平台相关,并不能完全确定。
在⽬标⽂件中指定的地址都是逻辑地址,符号真正的地址需要到链接阶段时进⾏相应的重定位以确定最终的地址。
在接下来的四⾏中
0000000000000004 b std::__ioinit
0000000000000008 D g_val
000000000000000c d func1()::val_static
0000000000000035 T func1(char*)
b在全局数据段中的4地址,因为上述g_uninit占⽤了四字节,所以std::__ioinit的地址为0+4=4.
⽽g_val存在于全局数据段(D)中,起始地址为8,在程序定义中,因为在0地址处存放的是str指针,⽽我的电脑系统为64位,所以指针长度为8,则g_val的地址为0+8=8
⽽静态变量val_static则是放在全局数据段8+sizeof(g_val)=12处
函数func1(char*)则放在代码段func1()后⾯
讲到这⾥,有些细⼼的朋友就会疑惑了,在全局数据区(D)中存放了str指针,那str指针指向的字符串放到哪⾥去了?其实这些字符串内容放在常量区,常量区属于代码区(t)(X86平台,不同平台可能有不同策略),对应nm显⽰⽂件的这⼀部分:
00000000000000a0    t _GLOBAL__sub_I_str
如果你对此有⼀些疑惑,你可以尝试将str字符串放⼤,甚⾄是改成上千个字节的字符串,就会看到代码段(t)的变化。
好了,关于linux下nm命令的解析就到此为⽌啦,如果朋友们对于这个有什么疑问或者发现有⽂章中有什么错误,欢迎留⾔
原创博客,转载请注明出处!
祝各位早⽇实现项⽬丛中过,bug不沾⾝.

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。