关于全局变量的地址总是在变这件事

作者: zzidun | 时间: 2022年03月07日 00:00:00 | 标签: 编程学习

起因

如果我们写这样的代码

/* a.cpp */
#include <stdio.h>

static int a = 1;

int main()
{
    printf("%p\n", &a);
    return 0;
}

然后用命令g++ a.cpp编译.

会发现每次执行都输出了不同的地址.

这是因为默认开启了pic,则会让程序产生位置无关的代码.

程序每次执行,会把各段数据放到一个随机的位置.只保证text段,rodata段等等内容的相对位置确定.

也就是说每次执行,text段的地址不确定,那么取地址的相关指令所在的位置也不确定.

执行到取地址的指令时,pc寄存器所存的地址也不确定.

但是由于各段数据的相对位置是确定的,我们可以知道pc所在的text段,和全局变量所在的rodata段的距离.

这时候只需要求pc+偏移,就可以知道全局变量所在的地址.

这时候就会每次执行输出不同的地址.

汇编代码对比

我们对比riscv64-linux-gnu-gcc编译上面的代码,生成的汇编代码.

我们只能用-S参数编译,而不能链接.

因为关闭pic之后,链接会报错.

前者是关闭pic,也就是加上-fno-pic参数.

    .file	"a.cpp"
    .option nopic
    .text
    .section	.sdata,"aw"
    .align	2
    .type	_ZL1a, @object
    .size	_ZL1a, 4
_ZL1a:
    .word	1
    .section	.rodata
    .align	3
.LC0:
    .string	"%p\n"
    .text
    .align	1
    .globl	main
    .type	main, @function
main:
.LFB0:
    .cfi_startproc
    addi	sp,sp,-16
    .cfi_def_cfa_offset 16
    sd	ra,8(sp)
    sd	s0,0(sp)
    .cfi_offset 1, -8
    .cfi_offset 8, -16
    addi	s0,sp,16
    .cfi_def_cfa 8, 0
    lui	a5,%hi(_ZL1a)
    addi	a1,a5,%lo(_ZL1a)
    lui	a5,%hi(.LC0)
    addi	a0,a5,%lo(.LC0)
    call	printf
    li	a5,0
    mv	a0,a5
    ld	ra,8(sp)
    .cfi_restore 1
    ld	s0,0(sp)
    .cfi_restore 8
    .cfi_def_cfa 2, 16
    addi	sp,sp,16
    .cfi_def_cfa_offset 0
    jr	ra
    .cfi_endproc
.LFE0:
    .size	main, .-main
    .ident	"GCC: (Ubuntu 10.3.0-8ubuntu1) 10.3.0"
    .section	.note.GNU-stack,"",@progbits

后者是默认的

    .file	"a.cpp"
    .option pic
    .text
    .data
    .align	2
    .type	_ZL1a, @object
    .size	_ZL1a, 4
_ZL1a:
    .word	1
    .section	.rodata
    .align	3
.LC0:
    .string	"%p\n"
    .text
    .align	1
    .globl	main
    .type	main, @function
main:
.LFB0:
    .cfi_startproc
    addi	sp,sp,-16
    .cfi_def_cfa_offset 16
    sd	ra,8(sp)
    sd	s0,0(sp)
    .cfi_offset 1, -8
    .cfi_offset 8, -16
    addi	s0,sp,16
    .cfi_def_cfa 8, 0
    lla	a1,_ZL1a
    lla	a0,.LC0
    call	printf@plt
    li	a5,0
    mv	a0,a5
    ld	ra,8(sp)
    .cfi_restore 1
    ld	s0,0(sp)
    .cfi_restore 8
    .cfi_def_cfa 2, 16
    addi	sp,sp,16
    .cfi_def_cfa_offset 0
    jr	ra
    .cfi_endproc
.LFE0:
    .size	main, .-main
    .ident	"GCC: (Ubuntu 10.3.0-8ubuntu1) 10.3.0"
    .section	.note.GNU-stack,"",@progbits

主要区别在main函数中:

前者向printf传参的语句如下

    lui	a5,%hi(_ZL1a)
    addi	a1,a5,%lo(_ZL1a)
    lui	a5,%hi(.LC0)
    addi	a0,a5,%lo(.LC0)
    call	printf

后者向printf传参的语句如下

    lla	a1,_ZL1a
    lla	a0,.LC0
    call	printf@plt

lla a1, _ZL1a等同于相当于以下指令

auipc a5,%hi(_ZL1a)
addi a1,a5,%lo(_ZL1a)

所以后者可以翻译为

    auipc a5,%hi(_ZL1a)
    addi a1,a5,%lo(_ZL1a)
    auipc a5,%hi(.LC0)
    addi a0,a5,%lo(.LC0)
    call	printf@plt

lui rd,imm的作用是把立即数imm左移12位,放到寄存器rd的高20位上.

auipc rd,imm的作用是把立即数imm左移12位+寄存器pc的结果,放到寄存器rd的高20位上.

addi rd1,rd2,imm的作用是把寄存器rd2+立即数imm的结果存到寄存器rd1.

有人会问,这里为什么需要用两条指令来做这个移动.

首先riscv的指令长度都是32位,这个32位中,需要用5位来表示rd寄存器,7位表示操作类型.

那么就只剩下20位了,所以一条指令只能给寄存器传20位的立即数.

所以这里采用了先传20位到寄存器的高位,再用加法把低12位加进去的方法.

指令如下:

这里就可以发现,前者每次给printf传入的参数都是固定的数值,后者传入的参数是pc+固定数值.

这个固定数值就是pc相对于目标的偏移.

因为每次text段被加载到不同的地址,所以每次执行,寄存器pc的值都不同,输出就不同了.