起因
如果我们写这样的代码
/* 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
的值都不同,输出就不同了.