C++冷知识第【四】期 变量与常量的底层表示

引言

  • 经过前几期的介绍,我们已经知道了函数中的局部变量是存储在 rbprsp 寄存器构成的栈帧之间的。本期我们将会继续深入介绍变量,另外看一下变量和常量在底层的汇编中有什么区别。

变量

局部变量

  • 变量是存储在 rbsrsp 寄存器指向地址构成的栈帧之间的,那么它们之间的顺序是什么样的,函数的参数变量和函数内定义的变量有什么区别。
  1. 编制下面的程序,并在 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;
    }
    
  2. 对应的完整的汇编代码如下:

    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 函数部分的汇编可以看到,传参时用到了两个通用寄存器 esiedi 。这个两个寄存器的值会在 func 函数内部赋值给 edxeax 寄存器。至于为什么需要让寄存器赋值两次我们会在后面介绍函数的篇章中详细探究。
    • 从上面的汇编代码中可以看出,不管是函数参数上的变量还是函数内定义的变量都是通过 rpb 寄存器从内存中提取的。也就是说不管时参数变量还是函数内的局部变量都是存储在栈帧中的。
  3. 对于 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) 来生成汇编代码。
  1. 编制以下程序

    char _1byte = 1;
    short _2byte = 2;
    int _4byte = 4;
    long _8byte = 8;
    
    int main(){
        _1byte ++;
        _2byte ++;
        _4byte ++;
        _8byte ++;
        _8byte ++;
        _4byte ++;
        _2byte ++;
        _1byte ++;
        return 0;
    }
    
  2. 使用下面命令生成包含标签的汇编代码:

    gcc main.c -S -masm=intel -o main.s
    
  3. 生成的包含标签的汇编代码如下:

    	.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+0x2ecfrip 指向下一条指令,所以它的地址是 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 声明的变量会成为静态变量,声明在函数体内的是局部静态变量,声明在函数体外的是全局静态变量。
局部静态变量
  • 局部静态变量在每次调用函数时都会保持之前调用结束后的值。也就是说它不会随所属函数的多次调用而被销毁以及重新初始化。
  1. 编制下面的程序,我们使用本地的编译器进行编译 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;
    }
    
    
  2. 用下面的命令生成Intel 风格的汇编代码:

    gcc main.c -S -masm=intel -o main.s
    
  3. 汇编代码如下

    	.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:
    
    
  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 加上偏移地址获得。并且它的标签的和全局变量的标签不完全相同,是由变量名 + . + 数字构成的。
  5. 接着查看定义它们的标签位置:

    	.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,, symbolN

      The .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,alignment

      The .comm directive allocates storage in the data 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,, expressionN

      The .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 修饰。
  1. 编制下面的代码

    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;
    }
    
    
    
  2. 使用本地 gcc 编译器生成汇编代码

    gcc main.c -S -masm=intel -o main.s
    
  3. 汇编代码如下

    	.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" // 右边是字符串字面量
    
  1. 编制下面的程序,来看看字面量在汇编中的表示

    int main(){
        int a = 10;
        double b = 10.0;
        bool c = true;
        const char* s = "hello";
        return 0;
    }
    
  2. C语言中没有布尔字面量,因此我们使用g++ 生成汇编代码

    g++ main.cpp -S -masm=intel -o main.s
    
  3. 汇编代码如下:

    	.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:
    
    
  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:
    
    • 可以看到整数字面量在汇编中是以 立即数 的形式展现的
  5. 接下来看浮点数,浮点数字面量根据标签 .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 段上的。
  6. 观察对应位置的汇编指令可以知道,布尔字面量也是用立即数表示的。true 对应立即数 1,false 对应立即数 0。

  7. 根据.LC1 标签可以定位到字符串字面量:

    	.section	.rodata
    .LC1:
    	.string	"hello"
    
    • 可以看到字符串字面量是用伪指令 .string 定义的。另它和浮点数字面量一样,字符串字面量也是定义在 .rodata 段,所属的标签都是 Local Constant

宏常量

  • 在 C/C++ 中可以使用 #define 来定义宏常量。宏常量实际上就是文本替换。C/C++ 程序在正式进行编译前需要首先对程序进行预处理,宏常量的文本替换就是在这个阶段进行的。我们可以通过下面的步骤验证这一点
  1. 首先编制下面的代码

    #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;
    }
    
  2. 使用下面的命令获得预处理后的代码

    g++ -E main.cpp -o main.ii
    
  3. 预处理后的代码

    # 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 常量。从底层的视角来看它和普通的变量有什么区别,又和字面量常量有什么区别
  1. 编制下面的代码

    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;
    }
    
  2. 对应的汇编代码如下

    	.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:
    
    
  3. 首先来看局部常量:

    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 关键字以后,从汇编的角度来看和没有加关键字的局部变量一样。那么为了保证这个变量不可变只能是编译器在生成汇编代码的时候进行检查。
  4. 全局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
      
  5. 总结一下,局部 const 常量在汇编层面与普通变量一样,应该是编译器在生成汇编代码的时候会对不可变性进行检查。而全局const常量、全局静态const常量、局部静态const常量 则会通过 .section .rodata 定义到只读内存区。毕竟局部 const 常量的作用域只在函数体内,随着栈帧的弹出会被销毁,作用域小让编译器容易对其不变性进行检查。局部静态 const 常量可以被任何调用该函数的地方访问到(如果作为引用或指针从函数返回),全局静态 const 常量可以被该文件中的其它函数访问到,全局 const 常量可以被任何其它链接到一起的文件访问到,他们的作用域就比较大了,导致编译器很难去检查它们是否被修改,所以就和字面量常量一样存储在 .rodata 内存段,用其他的机制来保证不变性。

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学艺不精的Антон

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值