说明:
本文采用Linux 内核 v3.10 版本
在学习 Linux Kernel 源码时,看到如下代码片段:
// file: arch/x86/kernel/head_64.S
startup_64:
/*
* Compute the delta between the address I am compiled to run at and the
* address I am actually running at.
*/
leaq _text(%rip), %rbp
subq $_text - __START_KERNEL_map, %rbp
那么_text(%rip)
的值该如何计算呢?
一、指令指针相对寻址
在求值之前,我们先来看下指令格式。这种带有 %rip
的指令,是 x86_64 架构下新增的指令格式,称为指令指针相对寻址(RIP-Relative Addressing),其计算方式为下一条指令的起始地址加上偏移量。偏移量是32位有符号整数,所以允许 ±2GB 的偏移范围。
A new addressing form, RIP-relative (relative instruction-pointer) addressing, is implemented in 64-bit mode. An
effective address is formed by adding displacement to the 64-bit RIP of the next instruction.
注:引用自 Intel 64 and IA-32 Architectures Software Developer Manuals Volume 2A Chapter 2 Instruction Format 2.2.1.6 RIP-Relative Addressing
其使用方式有两种:
- 偏移量为常数,比如
1234(%rip)
- 偏移量为符号,比如
symbol(%rip)
对于第一种情况,计算非常简单,直接把下一条指令地址加上常数就可以。
第二种情况的计算方式和第一种不同,不能把符号symbol
的地址和指令地址直接相加,要先计算从 RIP 到达符号 symbol
的偏移量,然后把偏移量与指令地址相加。换句话说,其指向的是符号symbol
的实际地址。
The x86-64 architecture adds an RIP (instruction pointer relative) addressing. This addressing mode is specified by using ‘rip’ as a base register. Only constant offsets are valid. For example:
AT&T: ‘1234(%rip)’, Intel: ‘[rip + 1234]’
Points to the address 1234 bytes past the end of the current instruction.
AT&T: ‘symbol(%rip)’, Intel: ‘[rip + symbol]’
Points to the
symbol
in RIP relative way, this is shorter than the default absolute addressing.
注:引用自 ld 官方文档: 9.16.7 Memory References
二、符号的值
说完了指令格式,我们再来看看_text
符号的值。
符号 _text
定义在链接脚本 arch/x86/kernel/vmlinux.lds.S
中:
// file: arch/x86/kernel/vmlinux.lds.S
#ifdef CONFIG_X86_32
...
#else
. = __START_KERNEL;
phys_startup_64 = startup_64 - LOAD_OFFSET;
#endif
/* Text and read-only data */
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
...
...
...
} :text = 0x9090
在链接脚本中,特殊符号 ”.“ 是指位置计数器,它总是指向当前输出位置;给符号 ”.“ 赋值,会导致位置计数器移动。关于位置计数器,可参考 ld 在线文档:3.10.5 The Location Counter。
在上述代码中,符号 _text
被设置为位置计数器的值;而在x86_64系统中,位置计数器被定义为__START_KERNEL
。宏 __START_KERNEL
定义在文件 arch/x86/include/asm/page_64_types.h
中:
// file: arch/x86/include/asm/page_64_types.h
#define __PHYSICAL_START ((CONFIG_PHYSICAL_START + \
(CONFIG_PHYSICAL_ALIGN - 1)) & \
~(CONFIG_PHYSICAL_ALIGN - 1))
#define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START)
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
其中,CONFIG_PHYSICAL_START
和 CONFIG_PHYSICAL_ALIGN
是 Linux Kernel 配置选项,其默认值如下:
// file: include/generated/autoconf.h
#define CONFIG_PHYSICAL_START 0x1000000 // 1M
#define CONFIG_PHYSICAL_ALIGN 0x1000000 // 1M
通过计算之后,宏 __START_KERNEL
为 0xffffffff81000000
。该值也是_text
的符号(虚拟)地址,从 vmlinx 上也可以查看到:
$ nm vmlinux|grep _text
...
ffffffff81000000 T _text
...
另外,我们也看到了另外一个宏 __PHYSICAL_START
,该宏定义的是内核代码(从保护模式开始)加载的物理地址,其值为0x1000000
( 1M);1M 以下是BIOS、bootloader、引导扇区、内核启动程序等的地址。宏__START_KERNEL_map
是内核加载到虚拟内存的起始地址,对应的是物理内存地址 0。
既然_text
符号的地址为 0xffffffff81000000
,那是否意味着 _text(%rip)
的计算结果就是该地址呢?先不着急下结论,我们来看下leaq _text(%rip), %rbp
指令编译后的结果。
在反编译之前,我们需要找到符号 startup_64
及下一个符号的地址:
$ nm vmlinux|sort -k1|grep -A 1 startup_64
0000000001000000 A phys_startup_64
ffffffff81000000 T startup_64
ffffffff81000000 T _text
ffffffff81000110 T secondary_startup_64
ffffffff810001b0 T start_cpu0
可以看到,startup_64
编译后的地址为 0xffffffff81000000
,其下一个符号 secondary_startup_64
的编译地址为 0xffffffff81000110
。然后使用 objdump命令来反编译 vmlinux:
$ objdump -d vmlinux --start-address=0xffffffff81000000 --stop-address=0xffffffff81000110
vmlinux: file format elf64-x86-64
Disassembly of section .text:
ffffffff81000000 <_text>:
ffffffff81000000: 48 8d 2d f9 ff ff ff lea -0x7(%rip),%rbp # ffffffff81000000 <_text>
ffffffff81000007: 48 81 ed 00 00 00 01 sub $0x1000000,%rbp
ffffffff8100000e: 48 89 e8 mov %rbp,%rax
ffffffff81000011: 25 ff ff 1f 00 and $0x1fffff,%eax
ffffffff81000016: 85 c0 test %eax,%eax
ffffffff81000018: 0f 85 a7 01 00 00 jne ffffffff810001c5 <bad_address>
ffffffff8100001e: 48 8d 05 db ff ff ff lea -0x25(%rip),%rax # ffffffff81000000 <_text>
ffffffff81000025: 48 c1 e8 2e shr $0x2e,%rax
...
...
可以看到,leaq _text(%rip), %rbp
编译后为 lea -0x7(%rip),%rbp
,其计算后的偏移量为 -0x7
。为什么是-0x7
呢,因为符号_text
的编译地址为 0xffffffff81000000
,当前指令长度为 7个字节,下一条指令的起始地址为0xffffffff81000007
;从下一条指令想要回到_text
处,需要减掉-0x7
。
可以看到, _text(%rip)
经过编译后变成了-0x7(%rip)
,其中已经没有符号地址了,变成了符号相对于%rip
的偏移量;其计算结果要看实际运行时 %rip
是多少,毕竟-0x7(%rip)
中只有一个变量%rip
。
那么此时%rip
的值是多少呢?
三、调试及结论
我们使用qemu + gdb
来调试一下。先在 startup_64
符号处打断点,然后运行,结果如下:
(gdb) file vmlinux
Reading symbols from vmlinux...
(gdb) target remote :1234
Remote debugging using :1234
0x000000000000fff0 in perf_throttled_count ()
(gdb) b startup_64
Breakpoint 1 at 0xffffffff81000000: file arch/x86/kernel/head_64.S, line 72.
(gdb) c
Continuing.
可以看到,虽然我们在 startup_64
(0xffffffff81000000
)处打了断点,但内核程序并没有在此处停止,而是一把梭哈到底。说明程序根本没有执行到该地址。
重新启动 qemu 和 gdb,在物理地址 0x1000000
处打断点,运行,结果如下:
(gdb) b *0x1000000
Breakpoint 1 at 0x1000000
(gdb) c
Continuing.
Breakpoint 1, 0x0000000001000000 in ?? ()
这一次,内核程序在断点处停下了,我们查看一下当前执行的指令:
(gdb) display /3i $pc
2: x/3i $pc
=> 0x1000000: lea -0x7(%rip),%rbp # 0x1000000
0x1000007: sub $0x1000000,%rbp
0x100000e: mov %rbp,%rax
结论:
可以看到,该地址处的指令,就是我们上文反汇编得到的startup_64
处的指令,所以:
_text(%rip)
的值为 0x1000000
而不是0xffffffff81000000
。
四、原因分析
为什么是这样呢?
在运行到startup_64
处时,虽然已经是64位模式了,并且开启了分页:
// file: arch/x86/boot/compressed/head_64.S
/* Enter paged protected Mode, activating Long Mode */
movl $(X86_CR0_PG | X86_CR0_PE), %eax /* Enable Paging and Protected mode */
movl %eax, %cr0
并且构建了早期页表:
// file: arch/x86/boot/compressed/head_64.S
/*
* Build early 4G boot pagetable
*/
/* Initialize Page tables to 0 */
leal pgtable(%ebx), %edi
xorl %eax, %eax
movl $((4096*6)/4), %ecx
rep stosl
/* Build Level 4 */
leal pgtable + 0(%ebx), %edi
leal 0x1007 (%edi), %eax
movl %eax, 0(%edi)
/* Build Level 3 */
leal pgtable + 0x1000(%ebx), %edi
leal 0x1007(%edi), %eax
movl $4, %ecx
1: movl %eax, 0x00(%edi)
addl $0x00001000, %eax
addl $8, %edi
decl %ecx
jnz 1b
/* Build Level 2 */
leal pgtable + 0x2000(%ebx), %edi
movl $0x00000183, %eax
movl $2048, %ecx
1: movl %eax, 0(%edi)
addl $0x00200000, %eax
addl $8, %edi
decl %ecx
jnz 1b
/* Enable the boot page tables */
leal pgtable(%ebx), %eax
movl %eax, %cr3
但由于程序还处于启动阶段,该页表仅仅是个临时页表,里面只映射了 4G 物理内存,而且物理内存和虚拟内存是一对一映射的,也就是说此时 0x1000000
既是物理内存地址,也是虚拟内存地址。而把内核加载到以 __START_KERNEL_map
为基准的虚拟地址,那是以后才干的事,现在还没完成。
五、参考资料
1、 Intel 64 and IA-32 Architectures Software Developer Manuals Volume 2A Chapter 2 Instruction Format 2.2.1.6 RIP-Relative Addressing
2、ld 在线文档: 9.16.7 Memory References
3、ld 在线文档:3.10.5 The Location Counter。
4、How do RIP-relative variable references like "[RIP + _a]" in x86-64 GAS Intel-syntax work?