1. Abstract
.
2. Introduction
在看一本内核代码分析书籍的时候,发现有一个地方总是不理解:在 arch/x86
/boot/header.S 文件里,有如下跳转指令 (2.6.24 内核中的 115 行 ) :
.byte 0xeb # short (2-byte) jump
.byte start_of_setup-1f
因为之前写汇编都是 jmp label 这样的写法,故而总是想不明白这里为什么要有两个标号去减。
其实原因是,我们这里的 jmp 都是 short jump( 对此不理解的请看汇编的书籍或在网络上搜寻 jmp 指令的含义 ) ,而 short jump 包括两部分,第一个部分是操作码,对于 short jump 而言就是 0xeb ,第二个部分是跳转距离,此距离指的是从当前汇编指令下面的一条汇编指令的起始地址开始计算的距离,也就是, short jump 的作用实际上是 : PC + JUMP_VALUE ,此 JUMP_VALUE 就是第二部分,即操作数部分。
如果我们在汇编代码里写的是 jmp label ,汇编编译程序会自动把 label 到当前汇编下一条汇编指令的地址作为操作数;
如果我们在汇编代码里涌了如 header.S 中的硬编码,那么 start_of_setup-1f 就是这两个 label 之间的距离了。
这么说吧,其实各个 label 都是一个地址,此地址在编译后为从 0 开始的地址,在链接之后成为一个从链接地址开始的地址,而 jmp label 的写法,汇编编译程序会自动变成 label – 下一个汇编指令的起始地址。
3. 分析和验证过程
3.1 主验证过程分析
如下的代码:
.code16
.section ".text", "ax"
.globl _start
_start:
jmp 1f
jmp start_of_setup
.byte 0xeb
.byte start_of_setup-1f
1:
mov $0x0000, %ax
label_test:
jmp start_of_setup
start_of_setup:
jmp 1b
编译、链接命令:
as --gstabs -o asm-test.o asm-test.s
ld -o asm-test asm-test.o
( 注,用 gcc 直接编译可以用如下命令: gcc -Wp,-MD -nostdinc -D__ASSEMBLY__ -Wa -c -o asm-test.o asm-test.s)
然后再反编译:
objdump -d asm-test
就会得到如下的汇编代码:
asm-test: file format elf32-i386
Disassembly of section .text:
08048054 <_start>:
8048054: eb 04 jmp 804805a <_start+0x6>
8048056: eb 07 jmp 804805f <start_of_setup>
8048058: eb 05 jmp 804805f <start_of_setup>
804805a: b8 00 00 eb 00 mov $0xeb0000,%eax
0804805d <label_test>:
804805d: eb 00 jmp 804805f <start_of_setup>
0804805f <start_of_setup>:
804805f: eb f9 jmp 804805a <_start+0x6>
由此可见,此处如下的两个写法完全是等效的:
1). jmp start_of_setup 和
2). .byte 0xeb
.byte start_of_setup-1f
同时,我们也可以看出,这里要求标号 1 一定要紧跟着硬编码的 jmp 指令之后,否则就跳转不到 start_of_setup 处了,比如说我们把汇编源代码略加修改,将标号 1 放后一点:
.code16
.section ".text", "ax"
.globl _start
_start:
jmp 1f
jmp start_of_setup
.byte 0xeb
.byte start_of_setup-1f
label_test:
jmp start_of_setup
1:
mov $0x0000, %ax
start_of_setup:
jmp 1b
然后经过编译、链接和反汇编的过程,得到的是如下的结果:
asm-test: file format elf32-i386
Disassembly of section .text:
08048054 <_start>:
8048054: eb 06 jmp 804805c <label_test+0x2>
8048056: eb 07 jmp 804805f <start_of_setup>
8048058: eb 03 jmp 804805d <label_test+0x3>
0804805a <label_test>:
804805a: eb 03 jmp 804805f <start_of_setup>
804805c: b8 00 00 eb fb mov $0xfbeb0000,%eax
0804805f <start_of_setup>:
804805f: eb fb jmp 804805c <label_test+0x2>
看到了吧,当前硬编码的 jmp 指令跳转到了 804805d 处,而这是 mov 指令的第二个字节,这是因为 start_of_setup 减去 1 的结果为 3 了。
3.2 编译和链接的区别
我再继续废话一点儿吧,估计这个所有的人都清楚,就是编译和链接结果的区别,编译之后,原则上说,所有的地址和 label 都是从 0 开始计算的,而链接过后,都是从链接地址开始计算的,我们以最开始的汇编源代码举例:
.code16
.section ".text", "ax"
.globl _start
_start:
jmp 1f
jmp start_of_setup
.byte 0xeb
.byte start_of_setup-1f
1:
mov $0x0000, %ax
label_test:
jmp start_of_setup
start_of_setup:
jmp 1b
编译、链接命令:
as --gstabs -o asm-test.o asm-test.s
ld -o asm-test asm-test.o
然后先反编译 编译的结果 asm-test.o :
命令: objdump -d asm-test.o
asm-test.o: file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
0: eb 04 jmp 6 <_start+0x6>
2: eb 07 jmp b <start_of_setup>
4: eb 05 jmp b <start_of_setup>
6: b8 00 00 eb 00 mov $0xeb0000,%eax
00000009 <label_test>:
9: eb 00 jmp b <start_of_setup>
0000000b <start_of_setup>:
b: eb f9 jmp 6 <_start+0x6>
可以看到所有地址都是从 0 地址开始计算的。
然后先反编译 链接的结果 asm-test :
命令: objdump -d asm-test
asm-test: file format elf32-i386
Disassembly of section .text:
08048054 <_start>:
8048054: eb 04 jmp 804805a <_start+0x6>
8048056: eb 07 jmp 804805f <start_of_setup>
8048058: eb 05 jmp 804805f <start_of_setup>
804805a: b8 00 00 eb 00 mov $0xeb0000,%eax
0804805d <label_test>:
804805d: eb 00 jmp 804805f <start_of_setup>
0804805f <start_of_setup>:
804805f: eb f9 jmp 804805a <_start+0x6>
可以看到所有的地址都是从链接地址 0x08048054 开始计算的,而 Linux 上 ELF 文件的缺省链接地址大多都是 0x08048xxx 这样的地址。
4. References
[1]. Linux 汇编语言开发指南 , http://www.ibm.com/developerworks/cn/linux/l-assembly/