编译器如何生成访问全局变量的代码?

Linux 专栏收录该内容
133 篇文章 0 订阅

在学习 riscv 架构的时候发现程序中的全局变量的访问是通过某个寄存器+偏移地址的方式来进行的。一直不清楚编译器编译时如何生成访问全局变量的代码,今天研究下 x86-64 架构上的一种常见的全局变量的访问方式。

使用如下代码进行研究测试:

#include <stdio.h>
#include <stdlib.h>

int running;
int running_two;

void bye(void)
{
	running = 1;
	running_two = 1;
}

int main(void)
{
    bye();

	exit(0);
}

上述代码中有两个未初始化的全局变量 running 与 running_two,这两个未初始化的全局变量将会被放到 .bss 段中。这里我将上述代码存储为 test.c 的源文件,对其进行编译。

gcc -c 生成可重定位目标文件

执行 gcc -c test.c 命令即可生成 test.o 文件。对 test.o 文件进行反汇编,找到 bye 函数的汇编码如下;

0000000000000000 <bye>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	c7 05 00 00 00 00 01 	movl   $0x1,0x0(%rip)        # e <bye+0xe>
   b:	00 00 00 
   e:	c7 05 00 00 00 00 01 	movl   $0x1,0x0(%rip)        # 18 <bye+0x18>
  15:	00 00 00 
  18:	90                   	nop
  19:	5d                   	pop    %rbp
  1a:	c3                   	retq   

在上面的反汇编输出中,两个 movl 指令分别是访问全局变量 running 与 running_two 的过程。

0x0(%rip) 与 0x0(%rip) 对应的是两个全局变量的地址,这里的寻址方式应当是寄存器寻址。而全局变量地址的访问,以 rip (pc) 寄存器的值为基址,0x0 只是一个占位符,并不表示实际的位置,它将会在链接过程中确定下来。这种方式被称为 RIP-relative addressing

查看重定位信息

执行 readelf 命令查看可重定位 section 的信息,输出如下:

[longyu@debian-10:23:02:13] tmp $ readelf -r test.o

重定位节 '.rela.text' at offset 0x278 contains 4 entries:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000000006  000800000002 R_X86_64_PC32     0000000000000004 running - 8
000000000010  000900000002 R_X86_64_PC32     0000000000000004 running_two - 8

R_X86_64_PC32 是重定位的类型,它被定义为 “S + A - P” 的形式。S、A、P 的函数如下:(摘录自Relocatable Global Data on x86

    S: Represents the value of the symbol whose index resides in the relocation entry.

    A: Represents the addend used to compute the value of the relocatable field.

    P: Represents the place of the storage unit being relocated.

对于第一个可重定位项,S 表示 running 符号的地址,A 的值为 -8,表示相对重定位偏移量之后的8个字节的位置,P 表示被重定位内容的存储位置,在这里它的值就是 6。在上文中反汇编 bye 函数的指令中,我们可以发现 6 这个偏移量是位于第一个 movl $0x1,0x0(%rip)
语句中,它其实就是计算出的 32-bit 偏移地址存放的地址。

生成可执行目标文件

链接脚本中指定 .bss 段的起始位置,.bss 段的起始位置确定后,running 与 running_two 在 .bss 段中的地址也就固定了。

反汇编链接后生成的可执行目标文件中的 bye 函数,有如下信息:

0000000000001135 <bye>:
    1135:       55                      push   %rbp
    1136:       48 89 e5                mov    %rsp,%rbp
    1139:       c7 05 f5 2e 00 00 01    movl   $0x1,0x2ef5(%rip)        # 4038 <running>
    1140:       00 00 00 
    1143:       c7 05 e7 2e 00 00 01    movl   $0x1,0x2ee7(%rip)        # 4034 <running_two>
    114a:       00 00 00 
    114d:       90                      nop
    114e:       5d                      pop    %rbp
    114f:       c3                      retq   

我们可以看到对 running 与 running_two 全局变量的访问被替换为了 rip 的值 + 偏移量的格式。

那这个偏移量是咋计算出来的呢?

其实用上文中我们提到过的 S + A -P 的公式就可以计算出这个偏移量。

running 全局变量 rip + addr 访问形式中,addr 使用下面的公式计算:

4038 -8 - 113b => 0x2ef5

这里的 4038 表示的是 running 的地址,它是 S,-8 表示 P 地址之后 8 个字节的位置,它是 A,113b 表示 32-bit 的偏移地址的存储位置, 它是 P。

S + A - P 就得到了 0x2ef5 这个偏移地址。由于机器是小端方式,低字节存储到低地址处,指令中存储的的就是 f5 2e.

RIP-relative addressing 中的 rip 体现在哪里?

上面的计算中没有体现 rip 的值。其实我们可以将上面的计算公式改为这样:4038 - (8 + 113b). 这里的 8 + 113b 就是 rip 指向的绝对地址,计算 running 符号与此地址的差值就能得到 rip + addr 的形式访问全局变量的 addr。

程序中段的内存布局

在这样的计算方式中, .bss 段位于高地址,text 段位于低地址,这就是常见的程序的内存布局。我们可以查看下面的图片,.text 段位于 low address。此图摘录自《APUE》。
程序的内存结构
我们也可以看出,.text 段与 .bss 段的距离是固定的,.data 段与 .bss 段的距离也是固定的。.data 与 .bss 中分配的变量的地址在编译期间就固定下来,尽管段的起始地址可能会变化,但是段间的偏移地址是固定的,这也是 rip + addressing 方式能够工作的基础。

对于 heap 与 stack 这种动态变化的段,其中分配的变量就不能通过这种方式来访问了。stack 中分配的变量一般是通过与栈底指针 + 偏移来访问的。而 heap 中分配的地址,常常要依赖全局变量来完成。

参考文章

Rekicatabke Global Data on x86
Understanding the x64 code models

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值