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

起因

如果我们写这样的代码

/* 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位加进去的方法.

指令如下:

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

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

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

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

posted on 2024-03-20 00:18  质钝_zzidun  阅读(19)  评论(0编辑  收藏  举报