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

  • fPIC;
  • fno-pic;

起因

如果我们写这样的代码

1
2
3
4
5
6
7
8
9
10
/* 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参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	.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

后者是默认的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
	.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传参的语句如下

1
2
3
4
5
lui	a5,%hi(_ZL1a)
addi a1,a5,%lo(_ZL1a)
lui a5,%hi(.LC0)
addi a0,a5,%lo(.LC0)
call printf

后者向printf传参的语句如下

1
2
3
lla	a1,_ZL1a
lla a0,.LC0
call printf@plt

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

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

所以后者可以翻译为

1
2
3
4
5
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位加进去的方法.

指令如下:

立即数 寄存器 操作符号
auipc 20位立即数 5位寄存器地址 0010111
lui 20位立即数 5位寄存器地址 0110111

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

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

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


关于全局变量的地址总是在变这件事
https://zzidun.tech/40b27038/
作者
zzidun pavo
发布于
2022年3月7日
许可协议