读书笔记《Linux内核完全剖析:基于0.12内核》——第三章 内核编程语言和环境

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&TIntel语法中几乎所有指令操作码的名称都相同,只有几个例外。例如,使用符号扩展从%al移动到%edxAT&T语句是”movsbl %al, %edx",即从bytelongbl,其他类似。
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.子区

汇编取得的字节数据通常位于 textdata区中。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把随后的语句汇编到编号为subsectiondata子区中。

7. .desc symbol, abs-expr

用绝对表达式的值设置符号symbol的描述符字段n_desc16位值。

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 使用eaxedx联合(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加载到eaxdest加载到edi
gcc会自动优化操作,
e.g.

asm("leal (%1, %1, 4), %0"
	: "=r"(y)
	: "0"(x));

该例子计算 x*5的值,其中%0(输出寄存器)和%1(输入寄存器)是gcc自动分配的寄存器。
注:如果输入寄存器为"0"或者为空的话,说明使用与相应输出一样的寄存器。
该例子中,若reax,则

”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 CC语言的另一个扩充是允许我们把一些变量值放到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语句以及递归函数。
编译时,可以使用选项-Winlinegcc对标志成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,最好使用inlineqstatic组合。否则需要使用选项 --std=gnu89
如果在定义一个函数时还指定了inlineextern关键词,那么该函数定义仅用于内联集成,并且在任何情况下都不会单独产生该函数自身的汇编代码,即使明确引用了该函数的地址也不会产生。
关键词inlineextern组合在一起的作用机会类同一个宏定义。使用这种组合方式就是把带有组合关键词的一个函数定义放在.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指向当前栈顶处的元素。
指令CALLRET用于处理函数调用和返回操作。
Intel惯例,
寄存器eax、edxecx的内容必须由调用者自己负责。
寄存器ebx、esiedi以及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使用两张编译器来生成内核代码文件,第一种是as86ld86,第二种是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. 执行头部分

目标文件的文件头中含有一个长度为32Bexec数据结构,通常称为文件头结构或执行头结构。
在这里插入图片描述
Linux 0.12系统使用了其中两种类型:
模块目标文件使用了OMAGIC (Old Magic)类型的a.out格式,它指明文件是目标文件或者是不纯的可执行文件。其魔数是0x107
执行文件使用了ZMAGIC类型的a.out格式,它指明文件为需求分页处理(demand-paging,即需求加载,load on demand )的可执行文件。其魔数是0x10b
这两种格式主要区别在于它们对各个部分的存储分配方式上。
执行头结构中的a_texta_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 链接程序预定义变量

在链接过程中,链接器ldld86会使用变量记录执行程序中每个段的逻辑地址。
链接预定义的外部变量通常至少有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个目标文件又是如何依赖于8C源文件和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)
lodsblodsw:把DS:SI指向的存储单元中的数据装入ALAX,然后根据DF标志(df=0)(df=1)SI
(2)
stosbstosw:把ALAX中的数据装入ES:DI指向的存储单元,然后根据DF标志(df=0)(df=1)DI
(3)
movsbmovsw:把DS:SI指向的存储单元中的数据装入ES:DI指向的存储单元中,然后根据DF标志分别(df=0)(df=1)SIDI
(4)
scasbscasw:把ALAX中的数据与ES:DI指向的存储单元中的数据相减,影响标志位,然后根据DF标志分别(df=0)(df=1)SIDI
(5)
cmpsbcmpsw:把DS:SI指向的存储单元中的数据与ES:DI指向的存储单元中的数据相减,影响标志位,然后根据DF标志分别(df=0)(df=1)SIDI
(6)
rep:重复其后的串操作指令。重复前先判断CX是否为0,为0就结束重复,否则CX1,重复其后的串操作指令。主要用在MOVSSTOS前。一般不用在LODS前。
上述指令涉及的寄存器:
段寄存器DSES、变址寄存器SIDI、累加器AX、计数器CX
涉及的标志位:DF、AF、CF、OF、PF、SF、ZF

Content

文章目录

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章