3.1 as86
汇编器
linux 0.1x
系统中使用了两种汇编器(Assembler)
。一种是能产生16
位代码的as86
汇编器,配套ld86
链接器;另一种是GNU
的汇编器gas(as)
,使用GNU ld
链接器。
编译器和链接器的源代码可以从FTP
服务器ftp.funet.fi
上或从网站www.oldlinux.org
下载。
3.1.1 as86
汇编语言语法
汇编器专门用来把低级语言程序编译成含机器码的二进制程序或目标文件。
as [options] -o objfile srcfile
3.1.2 as86
汇编语言程序
!
! boot.s -- bootsect.S frame-work.Usign code 0x07 replace 1 charater of string msg1 , and display on line one of screen..
!
.globl begtext, begdata, begbss, endtext, enddata, endbss !globl identifiers for ld86 link.
.text !body
begtext:
.data
begdata:
.bss !uninitialized data
begbss:
.text !body
BOOTSEG = 0x07c0 !BIOS loading original address of bootsect.
entry start !Notice program start from here.
start:
jmpi go, BOOTSEG !Segment jump.
go:
mov ax, cs
mov ds, ax
mov es, ax
mov [msg1+17], ah
mov cx, #20
mov dx, #0x1004 !line 17 & column 4 of screen
mov bx, #0x000c !red
mov bp, #msg1 !located the display string
mov ax, #0x1301 !write string and move cusor to the end.
int 0x10 !BIOS interrupt 0x10, function is 0x13, child fuction 01.
loop0:
jmp loop0
msg1: .ascii "Loading system ..."
.byte 13,10
.org 510 !It is mean start store follow statement from 510(0x1fe)
.word 0xaa55
.text
endtext:
.data
enddata:
.bss
endbss:
该程序是一个简单的引导扇区启动程序。编译链接产生的执行程序可以放入软盘第一个扇区直接用来引导计算机启动。启动后会在屏幕第17
行第5
列处显示红色字符串'Loading system ......
,并且光标下移一行。然后在第27
行死循环。
以感叹号’!
或者分号‘:’
开始的语句均为注释文字。
‘.globl'
是汇编指示符(或成为汇编伪指令、伪操作符)。汇编指示符均以一个字符’.'
开始,并且不会在编译时产生任何代码。
第14
行上的标识符entry
是保留关键字,用于迫使链接器ld86
在生成的可执行文件中包括进其后指定的标号start
。
第16
行上是一个段间(Inter-segment)
远跳转语句,就跳转到下一条指令。
3.1.3 as86
汇编语音程序的编译和链接
[/root]# as86 -0 -a -o boot.o boot.s //编译。生成与 as 部分兼容的目标文件。
[/root]# ld86 -0 -a -o boot.o boot.s //链接。去掉符号信息。
[/root]# dd bs=32 if=boot of=/dev/fd0 skip=1 //写入软盘或Image 盘文件中
3.1.4 as86
&ld86
的使用方法和选项
3.2 GNU as
汇编
上节介绍的as86
汇编器仅用于编译内核中的boot/bootsect.S
引导扇区程序和实模式下的设置程序boot/setup.s
。内核中其余所有汇编程序(包括C
语言产生的汇编程序)均使用gas
来编译。
3.2.1 编译as
汇编语言程序
as [ option ] [ -o objfile ] [ srcfile.s ... ]
3.2.2 as
汇编语法
as
汇编器使用AT&T
系统V
的汇编语法(以下简称AT&T
语法)。
1.汇编程序预处理
as
汇编器具有对汇编语言程序的简单预处理功能。
2.符号、语句和常数
符号(Symbol
)是由字符组成的标识符,组成符号的有效字符取自大小写字符集、数字和3
字符“-”。“.”、“$”
。
语句(Statement
)以换行符或者行分割字符“;
”作为结束。
若在一行的最后使用反斜杠字符"\"
(在换行符前),可以使语句使用多行。
语句由零个或多个标号(label
)开始,后面可以跟随一个确定语句类型的关键符号。
表3-1
as
汇编器支持的转义字符序列
转义码 | 说明 |
---|---|
\b |
退格符(Backspace ),值为0x08 |
\f |
换页符(FormFeed) , 值为0x0C |
\n |
换行符(NewLine) ,值为0x0A |
\r |
回车符(Carriage-Return) ,值为0x0D |
\NNN |
3 个八进制表示的字符代码 |
\xNN... |
16 进制数表示的字符代码 |
\\ |
反斜杠字符 |
\" |
表示双引号 |
3.2.3 指令语句、操作数和寻址
指令(Instructions)
是CPU
执行的操作,通常也称操作码(Opcode)
。
操作数(Operand)
是指令操作的对象。
地址(Address)
是指定数据在内存中的位置。
指令语句运行时通常由4
部分:标号,操作码,操作数,注释。
操作数可以是立即数、寄存器值、内存值。一个间接操作数(Indirect Operand)
含有实际操作数值的地址值。
#1) 立即操作数前需要加一个“$"
字符前缀。
#2) 寄存器名前需要加一个”%“
字符前缀。
#3) 内存操作数由变量名或者含有变量地址的一个骑车去指定。变量名隐含指出了变量的地址,并指示CPU
引用该地址处内存的内容。
1.指令操作码的命名
AT&T
语法中指令操作码名称(指令助记符)最后一个字符用来指明操作数的宽度。
AT&T
与Intel
语法中几乎所有指令操作码的名称都相同,只有几个例外。例如,使用符号扩展从%al
移动到%edx
的AT&T
语句是”movsbl %al, %edx"
,即从byte
到long
是bl
,其他类似。
表3-2``AT&T
语法与Intel语法中转换指令的对应关系
AT&T |
Intel |
说明 |
---|---|---|
cbtw |
cbw |
把%al 中的字节值符号扩展到%ax 中 |
cwtl |
cwde |
把%ax 中的字节值符号扩展到%eax 中 |
cwtd |
cwd |
把%ax 中的字节值符号扩展到%dx:%ax 中 |
cltd |
cdq |
把%eax 中的字节值符号扩展到%edx:%eax 中 |
2.指令操作码前缀
操作码前缀用于修饰随后的操作码。
例如,串扫描指令scas
使用前缀执行重复操作:
repne scas %es:(%edi), %al
表3-3
操作码前缀列表
操作码前缀 | 说明 |
---|---|
cs,ds,ss,es,fs,gs |
区覆盖操作码前缀。通过指定使用 区:内存操作数 内存引用形式会自动添加这种前缀 |
data16,addr16 |
操作数\地址宽度前缀。这两个前缀会把32 位操作数\地址改变为16 位的操作数\地址。注意,as 不支持16 位寻址方式。 |
lock |
总线锁存前缀。用于在指令执行期间禁止中断(仅对某些指令有效,参见80x86 手册)。 |
wait |
协处理器指令前缀。等待协处理器完成当前指令的执行。对于80386/80387 组合用不着这个前缀 |
rep,repe,repne |
串指令操作前缀。使串指令重复执行%ecx 中指定的次数 |
3.内存引用
Intel
语法的间接内存引用形式:
section:[base + index*scale + disp]
AT&T
语法形式:
section:disp(base, index, scale)
At&T
引用例子:
movl var, %eax # 把内存地址`var`处的内容放入寄存器`%eax`中。
movl %cs:var, %eax # 把代码段中内存地址 var 处的内容放入 %eax 中。
movb $0x0a, %es:(%ebx) # 把字节值 0x0a 保存到 es 段的 %ebx 指定的偏移处。
movl $var, %eax # 把 var 的地址放入 %eax 中。
movl array(%esi), %eax # 把 array+%esi 确定的内存地址处的内容放入 %eax 中。
movl (%ebx, %esi, 4), %eax # 把 %ebx+%esi*4 确定的内存地址处的内容放入 %eax 中。
movl array(%ebx, %esi, 4), %eax # 把 array+%ebx+%esi*4 确定的内存地址处的内容放入 %eax 中。
movl -4(%ebp), %eax # 把 %ebp -4 内存地址处 的内容放入 %eax 中,默认段 %ss 。
movl foo(, %eax, 4), %eax # 把内存地址 foo + %eax * 4 处内容放入 %eax 中, 默认段 %ds 。
4.跳转指令
跳转指令用于把执行行点转移到程序另一个位置处继续执行下去。
jmp NewLoc # 直接跳转。无条件直接跳转到标号 NewLoc 处继续执行。
jmp *%eax # 间接跳转。寄存器 %eax 的值是跳转的目标位置。
jmp *(%eax) # 间接跳转。从 %eax 指明的地址处读取跳转的目标位置。
3.2.4 区与重定位
区(Section)
(也称为段、节或部分)用于表示一个地址范围,操作系统讲会以相同的方式对待和出来在该地址范围中的数据信息。
链接器ld
会把输入的目标文件中的内容按照一定规律组合生成一个可执行程序。
为区 分配运行时刻的地址的操作数就被称作重定位(Relocation)操作。
as
汇编器输出产生的目标文件中至少具有3
个区,正文(.text)
、数据(.data)
和区(.bss)
。
为了执行重定位操作,在每次涉及目标文件中的一个地址时,ld
必须知道:
#1) 目标文件中对一个地址的引用是从什么地方算起的?
#2) 该引用的字节长度是多少?
#3) 该地址引用的是哪一个区?(地址)- (区的开始地址)的值等于多少?
#4) 对地址的引用与指令计数器PC(Programing Counter)
相关么?
1.链接器涉及的区
2.子区
汇编取得的字节数据通常位于 text
和data
区中。as
汇编器允许利用子区(Subsection)
来将某个区中可能分布着一些不相邻的数据组在汇编后聚集在一起存放。
使用子区是可选的。如果不使用子区,那么所有对象都会被放在子区0
中。每个区都有一个位置计数器(Location Counter)
,它会对每个汇编进该区的字节进行计数。
3.bss
区
bss
区用于存储局部公共变量。
3.2.5 符号
标号(Label)
是后面紧随一个冒号的符号。
符号名以一个字母或"."、"_"
字符之一开始。
1.特殊点符号
特殊符号"."
表示as
汇编的当前地址。因此表达式"mylab:.long ."
就会把mylab
定义为包含它自己所处的地址值。给"."
赋值就如同汇编命令".org"
的作用。因此表达式".=.+4"
与“。space 4"
完全相同。
2.符号属性
除了名字以外,每个符号都有”值“和”类型“属性。根据输出格式不同,符号也可以具有辅助属性。如果不定义就使用一个符号,as
就会假设其所有均为0
。这指示该符号是一个外部定义的符号。
符号通常是32位的。ld
会对未定义符号的值进行特殊处理。符号的类型属性含有用于链接器和调试器的重定位信息、指示符号是外部的表示以及一些其他可选的信息。
3.2.6 as
汇编命令
汇编命令是指示汇编器操作方式的伪指令。
1. .align abs-expr1, abs-expr2, abs-expr3
.align
是存储对齐汇编命令,用于在当前子区中把位置计数器值设置(增加)到下一个指定存储边界处。
2. .ascii “string”…
从位置计数器所指当前位置为字符串分配空间并存储字符串,可使用逗号分开写出多个字符串。
3. .asciz “string”…
该汇编命令与".ascii"
类似,但是每个字符串后面会自动添加NULL
字符。
4. .byte expressions
该汇编命令定义0
个或多个用逗号分开的字节值。每个表达式的值是1
个字节。
5. .comm symbol, length
在.bss
区中声明一个命名的公共区域。
6. .data subsection
该汇编命令通知as
把随后的语句汇编到编号为subsection
的data
子区中。
7. .desc symbol, abs-expr
用绝对表达式的值设置符号symbol
的描述符字段n_desc
的16
位值。
8. .fill repeat,size,value
该汇编命令会产生数个(repeat
个)大小为size
字节的重复拷贝。
9. .global symbol
该汇编命令会使得链接器ld
能看见符号symbol
。
10. .int expressions(.long exoressions)
该汇编命令在某个区中设置0
个或多个整数值(8038系统为4B
,同 .long
)。每个用逗号分开的表达式的值就是运行时刻的值。
11. .lcomm symbol, length
为符号symbol
指定的局部公共区域保留长度为length
字节的空间。
12. .octa bignums
指定0
个或多个用逗号分开的16B
大数(.byte, .word, .long, .quad, .octa 分别对应1\2\4\8\16字节数)
。
13. .org new_lc, fill
把当前区的位置计数器设置为值 new_lc
。当位置计数器值增长时,所跳跃过的字节将被填入值fill
。
14. .quad bignums
指定0
个或多个用逗号分开的8B
大数bignums
。
15. .short expressions (.word expressions)
指定0
个或多个用逗号分开的2
字节数。
16. .space size, fill
产生size
个字节,每个字节填值fill
。
17. .string “string”
定义一个或多个用逗号分开的字符串。
18. .text subsection
通知as
把随后的语句汇编进编号为subsection
的子区中。
3.2.7 编写16
位代码
as
不区分16
位和32
位汇编语句,取决于.code16
还是.code32
。
3.2.8 AS
汇编器命令行选项
-a
:开启程序列表
-f
:快速操作
-o
:指定输出的目标文件名。
-R
:组合数据区和代码区。
-W
:取消警告信息。
3.3 C
语言程序
3.3.1 C
程序编译和链接
3.3.2 嵌入式汇编
基本格式:
asm("汇编语句"
:输出寄存器
:输入寄存器
:会被修改的寄存器);
除第一行以外,后面带冒号的行若不适用就都可以省略。
asm是内联汇编语句关键词;
”汇编语句“写汇编指令的地方;
”输出寄存器“表示当这段嵌入式汇编执行完之后,哪些寄存器用于存放输出数据(对应C
语言表达式值或一个内存地址)。
”输入寄存器“表示在开始执行汇编代码时,这里指向的一些寄存器中应存放的输入值。
”会被修改的寄存器“表示你已对其中列出的寄存器中的值进行了改动,gcc
编译器不能再依赖与它原先对这些寄存器加载的值。
e.g.
kernel/traps.c
# define get_seg_byte(seg, addr) \ //宏函数名称
({ \
register char _res; \ // 定义了一个寄存器变量 _res 。
_asm_("push %%fs; \ // 首先保存 fs 寄存器原值(段选择符)。
mov %%ax, %%fs; \ //然后用 seg 设置 fs 。
movb %%fs:%2, %%al; \ //取 seg:addr 处 1 字节内容到 al 寄存器中。
pop %%fs" \ //恢复 fs 寄存器原值。
:"=a" (_res) \ //输出寄存器列表。
:"0" (seg),"m" (*(addr))); \ //输入寄存器列表。
_res;})
此代码定义了一个嵌入式汇编语言宏函数。通常使用汇编语句最方便的方式是把它们放在一个宏内。
为了让 GCC 编译产生的汇编语言程序中寄存器前有一个百分号”%“,在嵌入式汇编语句寄存器名称前就必须写上两个百分号”%%“。
”=a"
中的a
称为加载代码,“=”
表示输出寄存器,并且其中的值讲被输出替代。
加载代码是CPU
寄存器、内存地址以及一些数值的简写字母代号。
第9
行表示在这段代码开始时将seg
放到eax
寄存器中,“0”
表示使用了与上面相同位置上的输出寄存器。
(*(addr))
表示一个内存偏移地址值。为了在上面汇编语句中使用该地址,嵌入式汇编程序规定把输出和输入寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以"%0"
开始,分别记为%0、%1、...%9
,。因此,输出寄存器的编号是%0
(这里只有一个输出寄存器),输入寄存器前一部分(“0“(seg))
的编号是%1
,而后部分的编号是%2
。上面地6
行上的%2
即代表(*(addr))
这个内存偏移量。
表3-4
常用寄存器加载代码说明
代码 | 说明 | 代码 | 说明 |
---|---|---|---|
a |
使用寄存器eax |
m |
使用内存地址 |
b |
使用寄存器ebx |
o |
使用内存地址并可以加载偏移量 |
c |
使用寄存器ecx |
I |
使用常数0-31 |
d |
使用寄存器edx |
J |
使用常量0-63 |
S |
使用寄存器esi |
K |
使用常数0-255 |
D |
使用寄存器edi |
L |
使用常量0-65535 |
q |
使用动态分配字节可寻址寄存器(eax, ebx, ecx, edx ) |
M |
使用常量0-3 |
r |
使用任意动态分配的寄存器 | N |
使用1 字节常量(0-255) |
g |
使用统一有效的地址即可(eax, ebx, ecx, edx 或内存变量) |
O |
使用常量0-31 |
A |
使用eax 与edx 联合(64 位) |
= |
输出操作数。输出值讲替换前值 |
+ |
表示操作数可读写 | & |
早起会变的(earlyclobber )操作数。表示使用玩操作数之前,内容会改变。 |
第4~7
行代码的作用:
首先,fs
段寄存器内容入栈;
其次,eax
段值赋给fs
段寄存器;
再者,fs:(*(addr))
所指定的字节放入al
寄存器中。
e.g.
asm("cld\n\t"
"rep\n\t"
"stol"
: /* 没有输出寄存器 */
: "c"(count-1), "a"(fill_value), "D"(dest)
: "%ecx", "%edi");
前3
行是通常的汇编语句,用来清方向,重复保存值\n\n\t
为换行符和制表符(对齐程序作用)。
第5
行的含义是:将count-1
的值加载到ecx
,将fill_value
加载到eax
,dest
加载到edi
。
gcc
会自动优化操作,
e.g.
asm("leal (%1, %1, 4), %0"
: "=r"(y)
: "0"(x));
该例子计算 x*5
的值,其中%0
(输出寄存器)和%1
(输入寄存器)是gcc
自动分配的寄存器。
注:如果输入寄存器为"0"
或者为空的话,说明使用与相应输出一样的寄存器。
该例子中,若r
为eax
,则
”leal (eax, eax, 4), eax"
可以通过关键词volatile
来取消gcc
自动优化,代码如下:
_asm_ _volatile_ (...);
关键词volatile
也可以放在函数名前修饰函数,通知gcc
该函数不会返回。
e.g.
mm/memory.c
31 volatile void do_exit(long code);
32
33 static inline vocatile void oom(void)
34 {
35 printk("out of memory\n\r");
36 do_exit(SIGSEGV);
37 }
练习:(未看懂)
字符串命令查看附件1
。
3.3.3 圆括号中的组合语句
花括号"{}"
用于把变量声明和语句组合成一个复合语句(组合语句)或一个语句块(等同于一条语句)。
组合语句的右花括号后面不再使用分好。
圆括号中的组合语句“({...})”
,可以在GNU C
中当一个表达式使用。
e.g.
({ int y = foo(); int z;
if (y > 0 ) z = y;
else z = -y;
3 + z; })
解释:
表达式(“3+z”)
的值等价于整个圆括号阔住的语句的值。若最后一句不是表达式,那么整个语句表达式具有void
属性,即没有值。该表达式里声明的任何局部变量都会在整块语句结束后失效。
该语句和普通表达式一样。例如:
int i = 该语句;
这种表达式通常用来定义宏。
e.g.
init/main.c
69 # define CMOS_READ(addr) ({ \ // 反斜杠连接两行语句
70 outb_p(0x80 | addr, 0x70); \ //首先向 I/O 端口 0x70输出欲读取的位置 addr。
71 inb_p(0x71); \ //然后从端口 0x71 读入该位置处的值作为返回值。
72 })
include/asm/io.h
05 # define inb(port) ({ \
06 unsigned char _v; \
07 _asm_ volatile ("inb %%dx, %%al":"=a" (_v):"d" (port)); \
08 _v; \
09 })
3.3.4 寄存器变量
GNU C
对C
语言的另一个扩充是允许我们把一些变量值放到CPU
寄存器中,即寄存器变量。
寄存器变量分全局变量和局部变量。
定义局部寄存器变量的形式:
register int res _asm_("ax");
ax
是变量res
希望使用的寄存器。
定义这样一个寄存器变量并不会专门保留这个寄存器不派其他用途,也并不保证编译出来的代码会把变量一直放在指定的寄存器中。
3.3.5 内联函数
在程序中,通过把一个函数声明为内联(inline)
函数,就可以让gcc
把函数的代码集成调用到该函数的代码中区。这样处理的函数可以区掉函数调用时进入和退出时间开销,从而宽度能够加快执行速度。
内联韩式嵌入调用者代码中的操作是一种优化操作,因此只有进行优化编译时才会执行代码嵌入处理。若编译过程中没有使用优化选项-O
,那么内联函数的代码就不会被真正地嵌入到调用者代码中,而是只作为普通函数调用来处理。
把一个函数声明为内联函数的方法是在函数声明中使用关键字"inline"
。
e.g
fs/inode.c
inline int inc( int *a)
{
(*a)++;
}
函数中的某些语句用法可能会使的内联函数的替换操作无法正常进行,或者不适合进行替换操作。
例如,使用了可变参数、内存分配函数malloca()
、可变长度数据类型变量、非局部goto
语句以及递归函数。
编译时,可以使用选项-Winline
让gcc
对标志成inline
但不能被替换的函数给出警告信息以及不能替换的原因。
当一个函数定义中既使用inline
关键字,又使用static
关键词,即像下面文件fs/inode.c
中的内联函数定义一样,那么如果所有对该内联函数的调用都被替换二集成在调用者代码中,并且程序中没有引用过该内联函数的地址,则该内联函数自身的汇编代码就不会被引用。
在这种情况下,除非我们在编译过程中使用选项 -fkeep-inline-function
,否则gcc
就不会为该内联函数自身生成生成汇编代码。
20 static inline void wait_on_inode(struct m_inode * inode)
21 {
22 cli();
23 while (inode->i_lock)
24 sleep_on(&inode->i_wait);
25 sti();
26 }
C99
默认”省略“了static
,为了兼容C99
,最好使用inlineq
和static
组合。否则需要使用选项 --std=gnu89
。
如果在定义一个函数时还指定了inline
和extern
关键词,那么该函数定义仅用于内联集成,并且在任何情况下都不会单独产生该函数自身的汇编代码,即使明确引用了该函数的地址也不会产生。
关键词inline
和extern
组合在一起的作用机会类同一个宏定义。使用这种组合方式就是把带有组合关键词的一个函数定义放在.h
头文件中,并且把不含关键词的另一个相同函数定义放在一个库文件中。此时头文件中的定义大多数对该函数的调用被替换嵌入。如果还有未被替换的对该函数的调用,那么就会使用(引用)程序文件中或库中的副本。
e.g.
include/string.h、lib/string.c
字符串命令查看附件1
。
3.4 C
与汇编程序的相互调用
为了提高代码执行效率,内核源代码中有的地方直接使用了汇编语言编制。
3.4.1 C
函数调用机制
函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移。数据传递通过函数参数和返回值来进行。
1. 栈帧结构和控制转移方式
大多数CPU
上的程序失效使用栈来支持函数调用操作。栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复以及用来存储局部数据。
单个函数调用操作所使用的栈部分被称为栈帧(stack frame
)结构。如图3-4
。.
栈结构的两端由两个指针来指定。
寄存器ebp
通常用作帧指针(frame pointer)
,
esp
则用作栈指针(stack pointer)
。
esp
会随数据出栈和入栈而移动。
栈是往低(小)地址放心扩展的,而esp
指向当前栈顶处的元素。
指令CALL
和RET
用于处理函数调用和返回操作。
Intel
惯例,
寄存器eax、edx
和ecx
的内容必须由调用者自己负责。
寄存器ebx、esi
和edi
以及ebp、esp
的内容必须由被调用者负责保护。
2. 函数调用示例
/* exch.c */
void swap( int *a, int *b)
{
int c;
c = *a, *a = *b, *b = c;
}
int main()
{
int a, b;
a = 16, b = 32;
swap( &a, &b);
return(a - b);
}
这两个函数的栈指针结构如图3-5
所示。
图中的位置信息相对于寄存器ebp
中的帧指针。
栈帧左边的数字指出了相对于帧指针的地址偏移值。
(在像GDB
这样的调试器中,这些数值都用2
的补码表示,e.g. -4 = 0xFFFF FFFC ; -12 = 0xFFFF FFF4
)
使用命令
gcc -Wall -S -o exch.s exch.c
生成C
程序对应的汇编程序exch.s
代码
3. main()
也是一个函数
在编译链接时它将会作为crt0.s
汇编程序的函数被调用。
crt0.s
是一个桩(stub)
程序,名称中的"ctr"
是“C run-time”
的缩写。
Linux 0.12
中的crt0.s
汇编程序如下:
3.4.2 在汇编程序中调用C
函数
汇编程序调用一个C
函数时,程序需要首先按照逆向顺序把函数参数压入栈,即函数最后(最右边)一个参数先入栈,如图3-6
。
在执行CALL
指令时,CPU
会把CALL
指令的下一条指令的地址压入栈中(图3-6
中的EIP
)。
即使没有事先将参数压入栈,被调用函数还是会以EIP
位置以上的栈中的其他内容作为自己的参数使用。
e.g. parts
3.4.3 在C
程序中调用汇编函数
包含两个函数的汇编程序callee.s
如下。
该汇编文件中的第1
个函数mywirte()
利用系统中断0x80
调用系统调用sys_write(int fd, char *buf, int count)
实现在屏幕上显示信息。对应的系统功能号
Note:C
语言调用汇编失败(待解决)。
3.5 Linux 0.12
目标文件格式
Linux 0.12
使用两张编译器来生成内核代码文件,第一种是as86
和ld86
,第二种是GNU
的汇编器as(gas)
和C
语言编译器gcc
以及相应的链接程序`gld``。
3.5.1 目标文件格式
a.out
文件是一种被称为汇编与链接输出(Assembly & linker editor output)
的目标文件格式。
a.out
格式7
个区的基本定义和用途是:
#1) 执行头部分(exec header)
。该部分中包含由一些参数(exec 结构)
,是有关目标文件的整体结构信息。例如代码和数据区的长度、未初始化数据区的长度、对应源程序文件名以及目标文件创建时间等。
内核使用这些参数把执行文件加载到内存中并执行,而链接程序(ld)
使用这些参数将一些模块文件组合成一个可执行文件。这是目标文件唯一必要的组成部分。
#2) 代码区(text segment0
.由编译器或汇编器生成的二进制指令代码和数据信息,含有程序执行时被加载到内存中的指令代码和相关数据。能以制度形式加载。
#3) 数据区(data segment)
。由编译器或汇编器生成的二进制指令和数据信息,这部分含有已经初始化过的数据,总是被加载到可读写的内存中。
#4) 代码重定位信息(text relocation)
。这部分含有供链接程序使用的记录数据。在组合目标模块文件时用于定位代码段中的指针或地址。当链接程序需要改变目标代码的地址时就需要修正和维护这些地方。
#5) 数据重定位部分(data relocation)
。类似于代码重定位部分的作用,但是用于数据段中指针的重定位。
#6) 符号表(symbol table)
。这部分用于含有供链接程序使用的记录数据。这些记录数据保存这模块文件中定义的全局符号以及需要从其他模块文件中输入的符号,或者是由链接器定义的符号,用于在模块文件之间对命名的变量和函数(符号)进行交叉引用。
#7) 字符串表部分(string table)
。该部分含有与符号名相对应的字符串,供调试程序调试目标代码,与链接过程无关。这些信息可包含源程序代码和行号、局部符号以及数据结构描述信息等。
1. 执行头部分
目标文件的文件头中含有一个长度为32B
的exec
数据结构,通常称为文件头结构或执行头结构。
Linux 0.12
系统使用了其中两种类型:
模块目标文件使用了OMAGIC (Old Magic)
类型的a.out
格式,它指明文件是目标文件或者是不纯的可执行文件。其魔数是0x107
。
执行文件使用了ZMAGIC
类型的a.out
格式,它指明文件为需求分页处理(demand-paging,即需求加载,load on demand )
的可执行文件。其魔数是0x10b
。
这两种格式主要区别在于它们对各个部分的存储分配方式上。
执行头结构中的a_text
和a_data
字段分别指明后面只读的代码段和可读写数据段的字节长度。
a_entry
字段指定了程序代码开始执行的地址,而a_syms、a_trsize和a_drsize
字段则分别说明了数据段后符号表、代码和数据段重定位信息的大小。
2. 重定位信息部分
重定位的功能有两个。一是当代码段被重定位到一个不同的基地址处时,重定位项则用于指出需要修改的地方。二是在模块文件中存在对未定义符号引用时,当此未定义符号最终被定义时链接程序就可以使用相应重定位项对符号的值进行修正。
3. 符号表和字符串部分
由于GNU gcc
编译器允许任意长度的标识符,因此标志符字符串都位于符号表后的字符串表中。
符号的主要类型包括:
#1) text、data或bss
指明是本模块文件中定义的符号。此时符号值是模块中该符号的可重定位地址。
#2) abs
指明符号是一个绝对的(固定的)不可重定位的符号。符号的值就是该固定值。
#3) undef
指明是一个本模块文件中未定义的符号。此时符号值通常为0
。
3.5.2 Linux 0.12
的目标文件格式
查看执行文件头结构的具体值
[/usr/root]# gcc -c -o name.o name.c
[/usr/root]# gcc -o name name.o
[/usr/root]#
[/usr/root]# hexdump -x name.o
[/usr/root]# objdump -h name.o
[/usr/root]# hexdump -x name | more
[/usr/root]# objdump -h name
删除执行文件中的符号表信息命令。
[/usr/root]# strip exch
磁盘上a.out
执行文件的各区在进程逻辑地址空间中的对应关系如图3-8
所示。
3.5.3 链接程序输出
链接程序对输入的一个或多个模块文件以及相关的库函数模块进行处理,最终生成相应的二进制执行文件或一个由所有模块组合而成的大模块文件。此过程中,链接程序的首要任务是给执行文件(或者输出的模块文件)进行空间分配操作。
每个模块文件中包括几种类型的段,链接程序的第二个任务就是把所有模块中相同类型的段组合连接在一起,在输出文件中为指定段类型形成单一一个段。
3.5.4 链接程序预定义变量
在链接过程中,链接器ld
和ld86
会使用变量记录执行程序中每个段的逻辑地址。
链接预定义的外部变量通常至少有etext、_etext、edata、_edate、end
和_end
。
下面程序可以显示出几个变量的地址。
运行结果为:
可以看出带与不带下划线"_"
符号的地址值是相同的。
3.5.5 System.map
文件
当运行GNU
链接器gld(ld)
时若使用了"-M"
选项,或者使用了"-nm“
命令,则会在标准输出设备(通常是屏幕)上打印处链接映像(link map)
信息,即指由链接程序产生的目标程序内存地址映像信息。其中列出了程序段装入到内存中的位置信息。具体有:
#1) 目标文件即符号信息映射到内存中的位置。
#2) 公共符号如何放置。
#3) 链接中包含的所有文件成员及其引用的符号。
在编译内核时,Linux/MakeFile
文件产生的System.map
文件就用于存放内核符号表信息。
符号表是所有内核符号机器对应地址的一个列表,当然也包括上面说明的_etext、_edata
和_end
等符号的地址信息。
符号表样例如下:
第1
栏指明符号值(地址);第2
栏是符号类型,指明符号位于目标文件的哪个区(Sections)
或其属性;第3
栏是对应的符号名称。
dmi_broken
的变量位于内核地址0x03441a0
处。
3.6 Make
程序和Makefile
文件
有关make
的详细使用方法请参考《GNU make使用手册》
。
3.6.1 Makefile
文件内容
一个Makefie
文件可以包括五种元素:显示规则、隐含规则、变量定义、指示符和注释信息。
**显示规则(explicit rules
)**用于指定何时以及怎么样重新编译一个或多个被称作规则的目标(rule's targets)
的文件。规则中明确列出了目标所依赖的被称作为目标的先决条件(或依赖)的其他文件,同时也会给出用于创建或更新目标的命令。
**隐含规则(implicit rules
**则是根据目标和对象的名称来确定何时和如何重新编译一个或多个被称作规则的目标的文件。
**变量定义(variable definitions
)**用于在一行上为一个变量定义一个文本字符串。
**指示符(directives)
**是make
的一个命令,用于指示其在读取makefile
文件时执行的特定操作。
**注释(comments)
**是指Makefile
文件以”#”
字符开始的文字部分。
3.6.2 Makefile
文件中的规则
简单的Makefile
文件中含有一些如下形式的规则。这些规则主要用来描述**操作对象(源文件和目标文件)**之间的依赖关系。
target
(目标)对象通常是指程序生成的一个文件的名称,
例如它可以是一个可执行文件或者一个以".o"
结尾的目标文件(Object file)
。
目标也可以是所要采取活动的名称,
例如“清理”("clean")
。
prerequisite
(先决条件或称依赖对象)是用以创建target
所必要或者依赖的一系列文件或其他目标。
command
(命令)是值make
所执行的操作,通常就是一些shell
命令,是生成target
需要执行的操作。
3.6.3 Makefile
文件示例
当make
依据Makefile
文件中的内容重新编译C
文件时, 仅会对每个修改过的C
文件进行重新编译。
Makefile
示例文件中的内容描述了一个名为eidt
的执行文件依赖于8
个目标文件的方式,以及这8
个目标文件又是如何依赖于8
个C
源文件和3
个头文件的。
要使用该Makefile
创建执行文件“edit
,只需在命令行上简单地键入make
即可。
若要使用该Makefile
从当前目录中删除编译得到的执行文件和所有目标文件,只需要键入make clean
。
在该Makefile
文件中,规则的目标包括执行文件edit
和.o
目标文件(object file)”main.o"
、”kbd.o"
等。先决条件(或依赖条件)文件是诸如"main.o"
和"defs.h"
等源文件。
当目标是一个文件时,那么其先决条件中的任何依赖条件被修改过时就需要进行重新编译或链接。
Makefile
中规则的目标和先决条件的下一行是shell
命令。
3.6.4 make
处理Makefile
文件的方式
默认情况下,make
会从Makefile
文件中第一个目标开始执行(不包括"."
开始的目标)。
该目标被称为Makefile
的默认最终目标(default goal)
。最终目标就是make
努力尝试更新的目标。
3.6.5 Makefile
中的变量
定义变量的格式:
objects = something ...
引用变量格式:
$objects
3.6.6 让make
自动推断命令----(point)
make
隐含规则:
根据目标文件的命名形式使用"cc -c"
命令根据相应的.c
文件更新对应的.o
文件。
e.g.
它会使用"cc -c main.c -o main.o"
把"main.c"
编译成"main.o"
。因此我们可以省略.o
目标文件规则中的命令。
当一个.c
文件被以这种方式自动地使用,那么它会被自动地添加到先决条件(依赖条件)中。因此我们可以省略规则先决条件中的".c"
文件----假定我们同时省略了命令。
上述示例可以更新为:
3.6.7 隐含规则中的自动变量
略
[附1] 字符串处理指令
(1)
lodsb
、lodsw
:把DS:SI
指向的存储单元中的数据装入AL
或AX
,然后根据DF
标志(df=0)
增(df=1)
减SI
(2)
stosb
、stosw
:把AL
或AX
中的数据装入ES:DI
指向的存储单元,然后根据DF
标志(df=0)
增(df=1)
减DI
(3)
movsb
、movsw
:把DS:SI
指向的存储单元中的数据装入ES:DI
指向的存储单元中,然后根据DF
标志分别(df=0)
增(df=1)
减SI
和DI
(4)
scasb
、scasw
:把AL
或AX
中的数据与ES:DI
指向的存储单元中的数据相减,影响标志位,然后根据DF
标志分别(df=0)
增(df=1)
减SI
和DI
(5)
cmpsb
、cmpsw
:把DS:SI
指向的存储单元中的数据与ES:DI
指向的存储单元中的数据相减,影响标志位,然后根据DF
标志分别(df=0)
增(df=1)
减SI
和DI
(6)
rep
:重复其后的串操作指令。重复前先判断CX
是否为0
,为0
就结束重复,否则CX
减1
,重复其后的串操作指令。主要用在MOVS
和STOS
前。一般不用在LODS
前。
上述指令涉及的寄存器:
段寄存器DS
和ES
、变址寄存器SI
和DI
、累加器AX
、计数器CX
涉及的标志位:DF、AF、CF、OF、PF、SF、ZF
Content
文章目录
- 3.1 `as86`汇编器
- 3.2 `GNU as`汇编
- 3.2.1 编译`as`汇编语言程序
- 3.2.2 `as`汇编语法
- 3.2.3 指令语句、操作数和寻址
- 3.2.4 区与重定位
- 3.2.5 符号
- 3.2.6 `as`汇编命令
- 1. .align abs-expr1, abs-expr2, abs-expr3
- 2. .ascii "string"...
- 3. .asciz "string"...
- 4. .byte expressions
- 5. .comm symbol, length
- 6. .data subsection
- 7. .desc symbol, abs-expr
- 8. .fill repeat,size,value
- 9. .global symbol
- 10. .int expressions(.long exoressions)
- 11. .lcomm symbol, length
- 12. .octa bignums
- 13. .org new_lc, fill
- 14. .quad bignums
- 15. .short expressions (.word expressions)
- 16. .space size, fill
- 17. .string "string"
- 18. .text subsection
- 3.2.7 编写`16`位代码
- 3.2.8 `AS`汇编器命令行选项
- 3.3 `C`语言程序
- 3.4 `C`与汇编程序的相互调用
- 3.5 `Linux 0.12`目标文件格式
- 3.6 `Make`程序和`Makefile`文件
- 3.6.1 `Makefile`文件内容
- 3.6.2 `Makefile`文件中的规则
- 3.6.3 `Makefile`文件示例
- 3.6.4 `make`处理`Makefile`文件的方式
- 3.6.5 `Makefile`中的变量
- 3.6.6 让`make`自动推断命令----`(point)`
- 3.6.7 隐含规则中的自动变量
- [附1] 字符串处理指令
- Content