0x01 C++的static关键字主要在以下场景中用到
1)static修饰全局变量和局部变量
全局变量属于数据,放在.data或.bss段,如果是常量字符串的话,那就放在.rodata段,接下来看一下一个普通全局变量和static全局变量有什么区别?
int gdata1 = 10;
static int gdata2 = 10;
int main()
{
return 0;
}
Linux下创建a.c文件,查看符号表:
delphi@delphi-vm:~$ g++ -c a.cpp -o a.o
delphi@delphi-vm:~$ objdump -t a.o
a.o: file format elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 a.cpp
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .data 00000004 _ZL6gdata2
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000000 g O .data 00000004 gdata1
00000000 g F .text 0000000a main
00000000 *UND* 00000000 __gxx_personality_v0
delphi@delphi-vm:~$
从上图的符号表可以看出,全局变量gdata1是“g”代表global符号,在链接阶段其他的obj文件是可以看到的,这样的符号是要参与链接过程符号解析的;gdata2是“l”代表一个local本地符号,这样的符号只能当前obj文件可见,是不参与链接符号解析过程的,因此多个源文件可以定义同名的static全局变量,不会产生重复定义的错误。
函数中的普通局部变量是属于指令,是存放在.text代码段上的,运行时系统给函数在栈上分配空间,函数的局部变量此时表示栈上的一段内存;但是static静态局部变量就成为数据了,放在.data或.bss段上了,程序一起来它就有内存,第一次运行到它的时候进行初始化,整个进程结束,它的内存才释放。
2)static修饰普通函数
static修饰普通函数和上面static修饰全局变量的意思是一样的,函数经过编译,产生一个函数符号,被static修饰后,就成为了一个“l”的local符号了,只能当前obj文件可见,不参与链接的符号解析,因此其它文件不能链接这个文件中的static函数。
3)static修饰成员方法和成员变量成为静态成员方法和静态成员变量
static修饰成员方法就变成静态成员方法了,形参不会再生成this指针,因此调用的时候不再依赖于对象,用类的作用域来调用就可以了;static修饰成员变量,那么此时这样的成员变量就不属于对象的,而是属于类的,它的内存已经放在.data或者.bss段上了,同样访问不需要依赖对象了,通过类作用域就能够访问,相当于是落在类作用域下的全局变量。
===============================
0x02 补充知识:查看符号表的工具&手法
2.1. 使用nm工具:
- nm是一个常用的工具,用于查看目标文件的符号表。
- 默认情况下,nm显示符号的地址、类型和名称。
- 运行以下命令查看符号表:
-
nm a.o
delphi@delphi-vm:~$ g++ -c a.cpp -o a.o
delphi@delphi-vm:~$ nm a.o
00000004 d _ZL6gdata2
U __gxx_personality_v0
00000000 D gdata1
00000000 T main
delphi@delphi-vm:~$
符号类型标识:
- 在nm工具的输出中,符号类型用单个字母表示,常见的有:
- T或t: 代码段(text section),通常是函数。
- D或d: 已初始化的数据段(data section)。
- B或b: 未初始化的数据段(bss section)。
- U: 未定义的符号。
- 符**号可见性**:
- 符号的可见性(全局或本地)通常通过符号的大小写来表示:
- 大写字母(如D、B、T)表示全局符号。
- 小写字母(如d、b、t)表示本地符号。
具体来分析这里:
gdata1
- 符号类型: D
- 在符号表输出中,gdata1的符号类型是D,表示它是一个全局符号,位于已初始化的数据段(.data段)。
- 符号可见性:
- gdata1是一个全局符号,全局符号意味着它可以被其他目标文件访问和链接。
- 在符号表中,它显示为gdata1,没有前缀或修饰符。
gdata2
- 符号类型: d
- gdata2的符号类型是d,表示它是一个本地符号,位于已初始化的数据段(.data段)。
- gdata2是一个static全局变量,意味着它在当前编译单元内可见,不会被导出到其他目标文件。
- 符号修饰: _ZL6gdata2
- _ZL是GCC编译器对static变量的修饰,表示这是一个本地符号(local)。
- 6gdata2是符号名的修饰,6表示符号名的长度。
总结一下:
- gdata1: 是一个全局变量,存储在.data段,初始化为10,可以被其他目标文件访问。
- gdata2: 是一个static全局变量,存储在.data段,初始化为10,仅在当前编译单元内可见,不会被导出。
- 在nm工具的输出中,通过符号类型的大小写可以判断符号的可见性。大写字母表示全局符号,小写字母表示本地符号。因此,gdata1是全局符号的结论是从其符号类型D得出的。
通过这些符号信息呢,可以了解变量的存储位置和可见性,这对于理解程序的链接和内存布局蛮有用。
2.2. 使用readelf工具:
- readelf可以提供更详细的符号信息,包括符号的绑定(Binding)属性。
- 运行以下命令查看符号表:
-
readelf -s a.o
– 注意:在输出中,Binding列会显示GLOBAL或LOCAL,表示符号的可见性。
- GLOBAL: 符号是全局的,可以被其他目标文件访问。
- LOCAL: 符号是本地的,仅在当前目标文件可见。
delphi@delphi-vm:~$ readelf -s a.o
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.cpp
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 2
4: 00000000 0 SECTION LOCAL DEFAULT 3
5: 00000004 4 OBJECT LOCAL DEFAULT 2 _ZL6gdata2
6: 00000000 0 SECTION LOCAL DEFAULT 5
7: 00000000 0 SECTION LOCAL DEFAULT 6
8: 00000000 0 SECTION LOCAL DEFAULT 4
9: 00000000 4 OBJECT GLOBAL DEFAULT 2 gdata1
10: 00000000 10 FUNC GLOBAL DEFAULT 1 main
11: 00000000 0 NOTYPE GLOBAL DEFAULT UND __gxx_personality_v0
delphi@delphi-vm:~$
2.3.使用objdump工具:
- objdump也可以用于查看符号表,提供详细的符号信息。
- 运行以下命令查看符号表:
-
objdump -t a.o
delphi@delphi-vm:~$ objdump -t a.o
a.o: file format elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 a.cpp
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .data 00000004 _ZL6gdata2
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000000 g O .data 00000004 gdata1
00000000 g F .text 0000000a main
00000000 *UND* 00000000 __gxx_personality_v0
delphi@delphi-vm:~$