引言
- 经过前几期的介绍,我们已经知道了函数中的局部变量是存储在
rbp
和rsp
寄存器构成的栈帧之间的。本期我们将会继续深入介绍变量,另外看一下变量和常量在底层的汇编中有什么区别。
变量
局部变量
- 变量是存储在
rbs
和rsp
寄存器指向地址构成的栈帧之间的,那么它们之间的顺序是什么样的,函数的参数变量和函数内定义的变量有什么区别。
-
编制下面的程序,并在 Compiler Explorer 中查看对应的汇编代码(选择 x86_64 gcc 11.4)
void func(char temp_1byte,short temp_2byte){ char _1byte = 1; short _2byte = 2; int _4byte = 4; long _8byte = 8; } int main(){ func(1,2); return 0; }
-
对应的完整的汇编代码如下:
func(char, short): push rbp mov rbp, rsp mov edx, edi mov eax, esi ; 传参 mov BYTE PTR [rbp-20], dl ; char temp_1byte = 1; mov WORD PTR [rbp-24], ax ; char temp_2byte = 2; ; 局部变量 mov BYTE PTR [rbp-1], 1 ; char _1byte = 1; mov WORD PTR [rbp-4], 2 ; short _2byte = 2; mov DWORD PTR [rbp-8], 4 ; int _4byte = 4; mov QWORD PTR [rbp-16], 8 ; int _8byte = 8; nop pop rbp ret main: push rbp mov rbp, rsp mov esi, 2 mov edi, 1 call func(char, short) mov eax, 0 pop rbp ret
- 关于函数参数变量的赋值:
- 从
main
函数部分的汇编可以看到,传参时用到了两个通用寄存器esi
和edi
。这个两个寄存器的值会在func
函数内部赋值给edx
和eax
寄存器。至于为什么需要让寄存器赋值两次我们会在后面介绍函数的篇章中详细探究。
- 从
- 从上面的汇编代码中可以看出,不管是函数参数上的变量还是函数内定义的变量都是通过
rpb
寄存器从内存中提取的。也就是说不管时参数变量还是函数内的局部变量都是存储在栈帧中的。
- 关于函数参数变量的赋值:
-
对于 x86_64 gcc 11.4 来说,函数的局部变量存储的位置相较于参数变量更靠高地址。先定义的变量会存储在更高的地址上。另外函数的参数变量是存储在它自己的栈帧中的。然而这个并非铁律,观察下面的汇编代码,它是源代码和上面相同,汇编代码是由 x86_64 clang 13.0.0 (assertions )生成的:
func(char, short): # @func(char, short) push rbp mov rbp, rsp mov ax, si mov cl, dil ; 传参 mov byte ptr [rbp - 1], cl mov word ptr [rbp - 4], ax ; 局部变量 mov byte ptr [rbp - 5], 1 mov word ptr [rbp - 8], 2 mov dword ptr [rbp - 12], 4 mov qword ptr [rbp - 24], 8 pop rbp ret main: # @main push rbp mov rbp, rsp sub rsp, 16 mov dword ptr [rbp - 4], 0 mov edi, 1 mov esi, 2 call func(char, short) xor eax, eax add rsp, 16 pop rbp ret
- Clang 编译的汇编代码中,参数变量所处地址要高于局部变量,另外Clang 编译器创建栈帧的方式和GCC不完全相同,所以在分析底层原理时非常依赖编译器和具体的机器。
全局变量
- 全局变量定义在函数体外面,在汇编伪代码中会以标签的形式定义,为了方便观察标签以及验证访问内存时地址的正确性,我们下面使用本地机器中的 GCC (Ubuntu 22.04 gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0) 来生成汇编代码。
-
编制以下程序
char _1byte = 1; short _2byte = 2; int _4byte = 4; long _8byte = 8; int main(){ _1byte ++; _2byte ++; _4byte ++; _8byte ++; _8byte ++; _4byte ++; _2byte ++; _1byte ++; return 0; }
-
使用下面命令生成包含标签的汇编代码:
gcc main.c -S -masm=intel -o main.s
-
生成的包含标签的汇编代码如下:
.file "main.c" .intel_syntax noprefix .text .globl _1byte .data .type _1byte, @object ; 指定类型为对象 .size _1byte, 1 ; 指定变量大小为1字节 _1byte: ; 全局变量 _1byte 的标签,和它的名称相同 .byte 1 ; 该伪指令定义了变量的大小和它的值 .globl _2byte ; 该伪指令用于声明为全局可见, .align 2 .type _2byte, @object .size _2byte, 2 _2byte: .value 2 .globl _4byte .align 4 .type _4byte, @object .size _4byte, 4 _4byte: .long 4 .globl _8byte .align 8 .type _8byte, @object .size _8byte, 8 _8byte: .quad 8 .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 movzx eax, BYTE PTR _1byte[rip] add eax, 1 mov BYTE PTR _1byte[rip], al movzx eax, WORD PTR _2byte[rip] add eax, 1 mov WORD PTR _2byte[rip], ax mov eax, DWORD PTR _4byte[rip] add eax, 1 mov DWORD PTR _4byte[rip], eax mov rax, QWORD PTR _8byte[rip] add rax, 1 mov QWORD PTR _8byte[rip], rax mov rax, QWORD PTR _8byte[rip] add rax, 1 mov QWORD PTR _8byte[rip], rax mov eax, DWORD PTR _4byte[rip] add eax, 1 mov DWORD PTR _4byte[rip], eax movzx eax, WORD PTR _2byte[rip] add eax, 1 mov WORD PTR _2byte[rip], ax movzx eax, BYTE PTR _1byte[rip] add eax, 1 mov BYTE PTR _1byte[rip], al mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:
-
从上边的汇编代码中可以看出,每一个全局变量都有自己的标签,当程序编译时,编译器会计算当前指令的下一条指令的地址到存储全局变量的内存地址的差值,确保能够正确访问到。这一点和算数运算那一期介绍浮点数的存储和访问类似。下面这段汇编代码是通过反编译工具
objdump
获得的汇编代码,我们可以验证一下。 -
首先用编译器编译好源代码:
gcc main.c -o main
-
用
objdump
生成 Intel 风格的汇编代码objdump -d main -M intel
-
main 函数部分的汇编如下:
0000000000001129 <main>: 1129: f3 0f 1e fa endbr64 112d: 55 push rbp 112e: 48 89 e5 mov rbp,rsp 1131: 0f b6 05 d8 2e 00 00 movzx eax,BYTE PTR [rip+0x2ed8] # 4010 <_1byte> 1138: 83 c0 01 add eax,0x1 113b: 88 05 cf 2e 00 00 mov BYTE PTR [rip+0x2ecf],al # 4010 <_1byte> 1141: 0f b7 05 ca 2e 00 00 movzx eax,WORD PTR [rip+0x2eca] # 4012 <_2byte> 1148: 83 c0 01 add eax,0x1 114b: 66 89 05 c0 2e 00 00 mov WORD PTR [rip+0x2ec0],ax # 4012 <_2byte> 1152: 8b 05 bc 2e 00 00 mov eax,DWORD PTR [rip+0x2ebc] # 4014 <_4byte> 1158: 83 c0 01 add eax,0x1 115b: 89 05 b3 2e 00 00 mov DWORD PTR [rip+0x2eb3],eax # 4014 <_4byte> 1161: 48 8b 05 b0 2e 00 00 mov rax,QWORD PTR [rip+0x2eb0] # 4018 <_8byte> 1168: 48 83 c0 01 add rax,0x1 116c: 48 89 05 a5 2e 00 00 mov QWORD PTR [rip+0x2ea5],rax # 4018 <_8byte> 1173: 48 8b 05 9e 2e 00 00 mov rax,QWORD PTR [rip+0x2e9e] # 4018 <_8byte> 117a: 48 83 c0 01 add rax,0x1 117e: 48 89 05 93 2e 00 00 mov QWORD PTR [rip+0x2e93],rax # 4018 <_8byte> 1185: 8b 05 89 2e 00 00 mov eax,DWORD PTR [rip+0x2e89] # 4014 <_4byte> 118b: 83 c0 01 add eax,0x1 118e: 89 05 80 2e 00 00 mov DWORD PTR [rip+0x2e80],eax # 4014 <_4byte> 1194: 0f b7 05 77 2e 00 00 movzx eax,WORD PTR [rip+0x2e77] # 4012 <_2byte> 119b: 83 c0 01 add eax,0x1 119e: 66 89 05 6d 2e 00 00 mov WORD PTR [rip+0x2e6d],ax # 4012 <_2byte> 11a5: 0f b6 05 64 2e 00 00 movzx eax,BYTE PTR [rip+0x2e64] # 4010 <_1byte> 11ac: 83 c0 01 add eax,0x1 11af: 88 05 5b 2e 00 00 mov BYTE PTR [rip+0x2e5b],al # 4010 <_1byte> 11b5: b8 00 00 00 00 mov eax,0x0 11ba: 5d pop rbp 11bb: c3 ret
- 首先看地址
1131
这里是第一次访问全局变量_1byte
的地方,rip
寄存器总是指向下一条需要执行的指令的地址,而当前指令的下一条指令地址为1138
,加上rip
的偏移地址0x2ed8
结果是:0x1138 + 0x2ed8 = 0x4010
。 - 然后看地址
113b
这里第二次访问全局变量_1byte
,负责奖运算结果存入对应变量地址,可以看到这次访问的地址是rip+0x2ecf
,rip
指向下一条指令,所以它的地址是1141
,那么0x1141 + 0x2ecf = 0x4010
。经过验证访问的内存是正确的。 - 其它变量的寻址这里就不再赘述了,感兴趣的同学可以自行验证。
- 首先看地址
-
-
我们前面几期文章的事件中或多或少都接触到了标签,标签属于汇编伪代码,它可以表示地址也可以表示地址的偏移量,在 x86 中一般标签后面会带有冒号。它方便程序员阅读以及定位内存,在编译的过程中会被汇编器转换成真实的地址。汇编代码中的标签一般名称上是无所谓的,不过有以下几种约定俗称的命名方式,它们标定了声明的内存地址的作用域。(以下内容参考 c++ - gcc编译器中的字符串存储名.LC0,.LC1是什么英文缩写? - SegmentFault 思否 )
.LCn
:是Local Constant
的缩写。即局部常量。讲解算数运算那期文章中浮点数字面量就是存储在这个标签下的。.LFBn
:是Local Function Beginning
的缩写。即函数体的起始点。.LFEn
:是Local Function Ending
的缩写。即函数体的终止点。.LBBn
:是Local Block Beginning
的缩写。即块的起始点。.LBEn
:是Local Block Ending
的缩写。即块的终止点。
-
以
.
为开头的还可以是伪指令,这些伪指令是给汇编器看的,不同的伪指令会有不同的功能(参考Assembler Directives - x86 Assembly Language Reference Manual (oracle.com) ):- 定义数据,比如
.long
,这样汇编器会为对应的数据留足空间 - 声明某个符号的可见域,比如
.local
(局部可见,不标记为全局的都是局部可见) 和.globl
( 全局可见),这样可以告诉链接器ld
在链接其它目标文件或库文件的时候能不能看到这个符号。是否可见是由链接器ld
检查的,CPU并不关心而且没有能力去检查一个符号是否可见,它只会无情地执行指令。(这个结论是我通过查询资料总结的,谨慎参考,如果你有不同的见解,欢迎在评论区一起讨论) - 定义段,比如
.text
,或.section
(.section
可以自定义段) 。关于段是什么,为什么需要段我们会在后面介绍链接相关的内容是具体探究。我们暂且先认为内存被划成了若干个部分,每个部分存储不同类型的数据,每个部分构成一个段。 - 内存对齐
.align
,加入一些冗余内存,让变量占据的内存为 4、8 的整数倍,方便CPU操作数据。与内存对齐相关的内容会在后面的篇章中详细介绍
- 定义数据,比如
静态变量
- C/C++ 中用关键字
static
声明的变量会成为静态变量,声明在函数体内的是局部静态变量,声明在函数体外的是全局静态变量。
局部静态变量
- 局部静态变量在每次调用函数时都会保持之前调用结束后的值。也就是说它不会随所属函数的多次调用而被销毁以及重新初始化。
-
编制下面的程序,我们使用本地的编译器进行编译 GCC (Ubuntu 22.04 gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0)
void func(){ static int val = 0; static int val1 = 6; ++val; ++val1; } int main(){ func(); func(); func(); return 0; }
-
用下面的命令生成Intel 风格的汇编代码:
gcc main.c -S -masm=intel -o main.s
-
汇编代码如下
.file "main.c" .intel_syntax noprefix .text .globl func .type func, @function func: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov eax, DWORD PTR val.1[rip] add eax, 1 mov DWORD PTR val.1[rip], eax mov eax, DWORD PTR val1.0[rip] add eax, 1 mov DWORD PTR val1.0[rip], eax nop pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size func, .-func .globl main .type main, @function main: .LFB1: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov eax, 0 call func mov eax, 0 call func mov eax, 0 call func mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size main, .-main .local val.1 .comm val.1,4,4 .data .align 4 .type val1.0, @object .size val1.0, 4 val1.0: .long 6 .ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:
-
首先观察函数
func
中如何访问局部静态变量。func: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov eax, DWORD PTR val.1[rip] ; 和全局变量一样通过rip指令加上偏移指令访问内存 add eax, 1 mov DWORD PTR val.1[rip], eax mov eax, DWORD PTR val1.0[rip] add eax, 1 mov DWORD PTR val1.0[rip], eax nop pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0:
- 和访问全局变量以及浮点数字面量一样通过指令寄存器
rip
加上偏移地址获得。并且它的标签的和全局变量的标签不完全相同,是由变量名 +.
+ 数字构成的。
- 和访问全局变量以及浮点数字面量一样通过指令寄存器
-
接着查看定义它们的标签位置:
.size main, .-main .local val.1 .comm val.1,4,4 .data .align 4 .type val1.0, @object ; 定义局部静态变量 val1.0 .size val1.0, 4 ; 定义它的大小为四个字节 val1.0: .long 6 .ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5
-
局部静态变量
val
在上面的伪指令中使用.local
进行声明。这个过程并不会指定该变量的大小,而是通过.comm
伪指令指定 参考(Assembler Directives - x86 Assembly Language Reference Manual (oracle.com) ):.local
symbol1,
symbol2,
…,
symbolNThe
.local
directive declares each symbol in the list to be local. Each symbol is defined in the input file and not accessible to other files. Default bindings for the symbols are overridden. Symbols declared with the.local
directive take precedence over weak and global symbols. (SeeSymbol Table Section in Oracle Solaris 11.1 Linkers and Libraries Guide for a description of global and weak symbols.) Because local symbols are not accessible to other files, local symbols of the same name may exist in multiple files. The.local
directive only declares the symbol to be local in scope, it does not define the symbol..local
指令会将列表中的每一个符号设置为局部可见的。.comm
name,
size,
alignmentThe
.comm
directive allocates storage in thedata
section. The storage is referenced by the identifier name. Size is measured in bytes and must be a positive integer. Name cannot be predefined. Alignment is optional. If alignment is specified, the address of name is aligned to a multiple of alignment..comm
指令会直接在数据段分配存储空间。这个存储空间会被具体的名称 name 所引用。 -
局部静态变量
val1
通过.size
指定大小,通过.long
指定数据.long
expression1,
expression2,
…,
expressionNThe
.long
directive generates a long integer (32-bit, two’s complement value) for each expression into the current section. Each expression must be a 32–bit value and must evaluate to an integer value. The.long
directive is not valid for the.bss
section.val1
没有显式指明是局部可见还是全局可见,根据val
变量的可见性推断不指定.globl
默认是局部可见的
-
全局静态变量
- 全局静态变量定义在函数体外,并且使用关键字
static
修饰。
-
编制下面的代码
char G_1byte = 1; short G_2byte = 2; int G_4byte = 4; long G_8byte = 8; static char GS_1byte = 1; static short GS_2byte = 2; static int GS_4byte = 4; static long GS_8byte = 8; int main(){ static char LS_1byte = 1; static char LS_2byte = 2; static char LS_4byte = 4; static char LS_8byte = 8; return 0; }
-
使用本地 gcc 编译器生成汇编代码
gcc main.c -S -masm=intel -o main.s
-
汇编代码如下
.file "main.c" .intel_syntax noprefix .text .globl G_1byte .data .type G_1byte, @object .size G_1byte, 1 G_1byte: .byte 1 .globl G_2byte .align 2 .type G_2byte, @object .size G_2byte, 2 G_2byte: .value 2 .globl G_4byte .align 4 .type G_4byte, @object .size G_4byte, 4 G_4byte: .long 4 .globl G_8byte .align 8 .type G_8byte, @object .size G_8byte, 8 G_8byte: .quad 8 .type GS_1byte, @object .size GS_1byte, 1 GS_1byte: .byte 1 .align 2 .type GS_2byte, @object .size GS_2byte, 2 GS_2byte: .value 2 .align 4 .type GS_4byte, @object .size GS_4byte, 4 GS_4byte: .long 4 .align 8 .type GS_8byte, @object .size GS_8byte, 8 GS_8byte: .quad 8 .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .data .type LS_8byte.3, @object .size LS_8byte.3, 1 LS_8byte.3: .byte 8 .type LS_4byte.2, @object .size LS_4byte.2, 1 LS_4byte.2: .byte 4 .type LS_2byte.1, @object .size LS_2byte.1, 1 LS_2byte.1: .byte 2 .type LS_1byte.0, @object .size LS_1byte.0, 1 LS_1byte.0: .byte 1 .ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:
-
可以看到相较于全局变量,全局静态变量没有
.globl
声明,这说明在其它的文件中是无法访问它的。.file "main.c" .intel_syntax noprefix .text .globl G_1byte .data ; 将当前段切换到数据段,下面的标签和数据是定义在数据段上的 .type G_1byte, @object .size G_1byte, 1 G_1byte: .byte 1 .globl G_2byte .align 2 .type G_2byte, @object .size G_2byte, 2 G_2byte: .value 2 .globl G_4byte .align 4 .type G_4byte, @object .size G_4byte, 4 G_4byte: .long 4 .globl G_8byte .align 8 .type G_8byte, @object .size G_8byte, 8 G_8byte: .quad 8 .type GS_1byte, @object .size GS_1byte, 1 GS_1byte: .byte 1 .align 2 .type GS_2byte, @object .size GS_2byte, 2 GS_2byte: .value 2 .align 4 .type GS_4byte, @object .size GS_4byte, 4 GS_4byte: .long 4 .align 8 .type GS_8byte, @object .size GS_8byte, 8 GS_8byte: .quad 8
-
从汇编的角度来看,局部静态变量和全局静态变量除了没有为每一个变量进行内存对齐操作(
.align
)并没有作用域上的差别。而在C/C++ 源代码中是无法在函数外直接访问局部静态变量。既然汇编代码上没有对这个作用域做出限制,可以推断局部静态变量的作用域限制是由编译器检查的(有没有在函数体外直接访问这个变量),而不是汇编器和链接器。-
全局静态变量
.type GS_1byte, @object .size GS_1byte, 1 GS_1byte: .byte 1 .align 2 .type GS_2byte, @object .size GS_2byte, 2 GS_2byte: .value 2 .align 4 .type GS_4byte, @object .size GS_4byte, 4 GS_4byte: .long 4 .align 8 .type GS_8byte, @object .size GS_8byte, 8 GS_8byte: .quad 8
-
局部静态变量,标签定义顺序和变量定义顺序相反
.type LS_8byte.3, @object .size LS_8byte.3, 1 LS_8byte.3: .byte 8 .type LS_4byte.2, @object .size LS_4byte.2, 1 LS_4byte.2: .byte 4 .type LS_2byte.1, @object .size LS_2byte.1, 1 LS_2byte.1: .byte 2 .type LS_1byte.0, @object .size LS_1byte.0, 1 LS_1byte.0: .byte 1
-
-
常量
字面量
-
字面量指的是一系列对固定值的表示,比如数字和字符串:
int a = 10; // 右边是整数字面量 double b = 10.0; // 右边是浮点数字面量 bool c = true; // 右边是布尔字面量 const char* s = "hello" // 右边是字符串字面量
-
编制下面的程序,来看看字面量在汇编中的表示
int main(){ int a = 10; double b = 10.0; bool c = true; const char* s = "hello"; return 0; }
-
C语言中没有布尔字面量,因此我们使用g++ 生成汇编代码
g++ main.cpp -S -masm=intel -o main.s
-
汇编代码如下:
.file "main.c" .intel_syntax noprefix .text .section .rodata .LC1: .string "hello" .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov DWORD PTR -20[rbp], 10 movsd xmm0, QWORD PTR .LC0[rip] movsd QWORD PTR -16[rbp], xmm0 mov BYTE PTR -21[rbp], 1 lea rax, .LC1[rip] mov QWORD PTR -8[rbp], rax mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .section .rodata .align 8 .LC0: .long 0 .long 1076101120 .ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:
-
首先来看整数字面量
main: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov DWORD PTR -20[rbp], 10 ; int a = 10; movsd xmm0, QWORD PTR .LC0[rip] ; double b = 10.0; movsd QWORD PTR -16[rbp], xmm0 mov BYTE PTR -21[rbp], 1 lea rax, .LC1[rip] mov QWORD PTR -8[rbp], rax mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0:
- 可以看到整数字面量在汇编中是以 立即数 的形式展现的
-
接下来看浮点数,浮点数字面量根据标签
.LC0
可以定位.section .rodata .align 8 .LC0: .long 0 .long 1076101120 .ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5
- 1076101120 是十进制下的浮点数的 IEEE 表示。在第二期文章中有详细介绍
.LC0
标签表示Local Constant
,意味着这下面的数据属于局部常量.section
伪指令可以自定义内存段的名称以及切换到对应的段,在.LC0
标签之前有一条.section .rodata
伪指令,这说明这个浮点数字面量是定义在.rodata
段上的。
-
观察对应位置的汇编指令可以知道,布尔字面量也是用立即数表示的。
true
对应立即数 1,false
对应立即数 0。 -
根据
.LC1
标签可以定位到字符串字面量:.section .rodata .LC1: .string "hello"
- 可以看到字符串字面量是用伪指令
.string
定义的。另它和浮点数字面量一样,字符串字面量也是定义在.rodata
段,所属的标签都是Local Constant
。
- 可以看到字符串字面量是用伪指令
宏常量
- 在 C/C++ 中可以使用
#define
来定义宏常量。宏常量实际上就是文本替换。C/C++ 程序在正式进行编译前需要首先对程序进行预处理,宏常量的文本替换就是在这个阶段进行的。我们可以通过下面的步骤验证这一点
-
首先编制下面的代码
#define PI 3.14159 #define STR "hello world" #define INT_VAL 10 #define TRUE_VAL true #define FALSE_VAL false int main(){ // a test of preprocessor double a = PI; const char* s = STR; int b = INT_VAL; bool boolval1 = TRUE_VAL; bool boolval2 = FALSE_VAL; return 0; }
-
使用下面的命令获得预处理后的代码
g++ -E main.cpp -o main.ii
-
预处理后的代码
# 0 "main.cpp" # 0 "<built-in>" # 0 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 0 "<command-line>" 2 # 1 "main.cpp" int main(){ double a = 3.14159; const char* s = "hello world"; int b = 10; bool boolval1 = true; bool boolval2 = false; return 0; }
- 可以看到所有引用到宏的地方都被替换成了对应的字面量。
- 另外预处理还会将注释语句删除掉。
const 修饰的常量
- 用
const
关键字声明的变量会成为const 常量。从底层的视角来看它和普通的变量有什么区别,又和字面量常量有什么区别
-
编制下面的代码
const char Gcc = 1; const short Gcs = 2; const int Gci = 4; const long Gcl = 8; static const char GScc = 1; static const short GScs = 2; static const int GSci = 4; static const long GScl = 8; int main(){ const char Lcc = 1; const short Lcs = 2; const int Lci = 4; const long Lcl = 8; static const char LScc = 1; static const short LScs = 2; static const int LSci = 4; static const long LScl = 8; return 0; }
-
对应的汇编代码如下
.file "main.c" .intel_syntax noprefix .text .globl Gcc .section .rodata .type Gcc, @object .size Gcc, 1 Gcc: .byte 1 .globl Gcs .align 2 .type Gcs, @object .size Gcs, 2 Gcs: .value 2 .globl Gci .align 4 .type Gci, @object .size Gci, 4 Gci: .long 4 .globl Gcl .align 8 .type Gcl, @object .size Gcl, 8 Gcl: .quad 8 .type GScc, @object .size GScc, 1 GScc: .byte 1 .align 2 .type GScs, @object .size GScs, 2 GScs: .value 2 .align 4 .type GSci, @object .size GSci, 4 GSci: .long 4 .align 8 .type GScl, @object .size GScl, 8 GScl: .quad 8 .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov BYTE PTR -15[rbp], 1 mov WORD PTR -14[rbp], 2 mov DWORD PTR -12[rbp], 4 mov QWORD PTR -8[rbp], 8 mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .section .rodata .align 8 .type LScl.3, @object .size LScl.3, 8 LScl.3: .quad 8 .align 4 .type LSci.2, @object .size LSci.2, 4 LSci.2: .long 4 .align 2 .type LScs.1, @object .size LScs.1, 2 LScs.1: .value 2 .type LScc.0, @object .size LScc.0, 1 LScc.0: .byte 1 .ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:
-
首先来看局部常量:
main: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov BYTE PTR -15[rbp], 1 ; const char Lcc = 1; mov WORD PTR -14[rbp], 2 ; const short Lcs = 2; mov DWORD PTR -12[rbp], 4 ; const int Lci = 4; mov QWORD PTR -8[rbp], 8 ; const long Lcl = 8; mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0:
- 有没有发现对一个局部变量加上一个
const
关键字以后,从汇编的角度来看和没有加关键字的局部变量一样。那么为了保证这个变量不可变只能是编译器在生成汇编代码的时候进行检查。
- 有没有发现对一个局部变量加上一个
-
全局const 常量、全局静态const 常量,以及局部静态 const 常量和普通的全局变量、全局静态变量、局部静态变量则有部分不同:
-
全局 const 常量定义前:
.globl Gcc .section .rodata .type Gcc, @object .size Gcc, 1 Gcc:
- 不同于普通的全局变量这里声明的内存段为
.rodata
段(read only data segment)。而普通全局变量声明的段是.data
(data segment)
- 不同于普通的全局变量这里声明的内存段为
-
全局静态常量也是同理,它紧接着全局 const 常量,没有使用切换内存段的伪指令
-
局部静态常量也是如此:
.size main, .-main .section .rodata .align 8 .type LScl.3, @object .size LScl.3, 8 LScl.3: .quad 8 .align 4 .type LSci.2, @object
-
-
总结一下,局部 const 常量在汇编层面与普通变量一样,应该是编译器在生成汇编代码的时候会对不可变性进行检查。而全局const常量、全局静态const常量、局部静态const常量 则会通过
.section .rodata
定义到只读内存区。毕竟局部 const 常量的作用域只在函数体内,随着栈帧的弹出会被销毁,作用域小让编译器容易对其不变性进行检查。局部静态 const 常量可以被任何调用该函数的地方访问到(如果作为引用或指针从函数返回),全局静态 const 常量可以被该文件中的其它函数访问到,全局 const 常量可以被任何其它链接到一起的文件访问到,他们的作用域就比较大了,导致编译器很难去检查它们是否被修改,所以就和字面量常量一样存储在.rodata
内存段,用其他的机制来保证不变性。