Linux内核入门(四)—— 内核汇编语言规则
来源:互联网 发布:动易网络 编辑:程序博客网 时间:2024/05/31 15:19
任何一个用高级语言编写的操作系统,其内核源代码中总有少部分代码是用汇编语言编写的。读过Unix Sys V源代码的读者都知道,在其约3万行的核心代码中用汇编语言编写的代码约2000行,分成不到20个扩展名为.s和.m的文件,其中大部分是关于中断与异常处理的底层程序,还有就是与初始化有关的程序以及一些核心代码中调用的公用子程序。
用汇编语言编写核心代码中的部分代码,大体上是出于如下几个方面的考虑的:
在Linux内核的源代码中,以汇编语言编写的程序或程序段,有几种不同的形式:
第一种是完全的汇编代码,这样的代码采用.s作为文件名的后缀。事实上,尽管是“纯粹”的汇编代码,现代的汇编工具也吸收了C语言预处理的长处,也在汇编之前加上了一趟预处理,而预处理之前的文件则以.S为后缀。此类(.S)文件也和C程序一样,可以使用#include, #ifdef等等成分,而数据结构也一样可以在.h文件中加以定义。
第二种是嵌入在C程序中的汇编语言片段。虽然在ANSI的C语言标准中并没有关于汇编片段的规定,事实上各种实际使用的C编译中都作了这方面的扩充,而GNU的C编译gcc也在这方面作了很强的扩充。
此外,内核代码中也有几个Intel格式的汇编语言程序,是用于系统引导的。由于我们专注于Intel i386系统结构下的Linux内核,下面我们只介绍GNU对i386汇编语言的支持
对于新接触Linux内核源代码的读者,哪怕他比较熟悉i386汇编语言,在理解这两种汇编语言的程序或片段时都会感到困难,有的甚至会望而却步。其原因是:在内核“纯”汇编代码中GNU采用了不同于常用386汇编语言的句法;而在嵌入C程序的片段中,则更增加了一些指导汇编工具如何分配使用寄存器、以及如何与C程序中定义的变量相结合的语言成分。这些成分使得嵌入C程序中的汇编语言片段实际上变成了一种介乎386汇编和C之间的一种中间语言。
所以,我们先集中地介绍一下在内核中这两种情况下使用的386汇编语言,以后在具体的情景中涉及具体的汇编语言代码时还会加以解释。
1 GNU的386汇编语言
在Dos/Windows领域中,386汇编语言都采用由Intel定义的语句(指令)格式,这也是几乎在所有的有关386汇编语言程序设计的教科书或参考书中所使用的格式。可是,在Unix领域中,采用的却是由AT&T定义的格式。当初,当AT&T将Unix移植到80386处理器上时,根据Unix圈内人上的习惯和需要而定义了这样的格式。Unix最初是在PDP-11机器上开发的,先后移植到VAX和68000系列的处理器上。这些机器的汇编语言在风格上、从而在格式上与Intel的有所不同。而AT&T定义的386汇编语言就比较接近那些汇编语言。后来,在Unixware中保留了这种格式。GNU主要是在Unix领域内活动的(虽然GNU是“GNU is Not Unix”的缩写)。为了与先前的各种Unix版本与工具有尽可能好的兼容性,由GNU开发的各种系统工具自然地继承了AT&T的386汇编语言格式,而不采用Intel的格式
那么,这两种汇编语言之间的差距到底有多大呢?其实是大同小异。可是有时候小异也是很重要的,不加重视就会造成困扰。具体讲,主要有下面这么一些差别:
MOV AL, BYTE PTR FOO(Intel格式)
movb FOO,%a1(AT&T格式) (5) 在AT&T格式中,直接操作数要加上“$”作为前缀,而在Intel格式中则不带前缀。所以, Intel格式中的"PUSH 4",在AT&T格式中就变为"pushl $4"。 (6) 在AT&T格式中,绝对转移或调用指令jump/call的操作数(也即转移或调用的目标地址), 要加上“*”作为前缀(读者大概会联想到C语言中的指针吧),而在Intel格式中则不带。 (7) 远程的转移指令和子程序调用指令的操作码名称,在AT&T格式中为“ljmp”和“lcall气而 在Intel格式中,则为"JMP FAR"和"CALL FAR"。当转移和调用的目标为直接操作数时, 两种不同的表示如下:
CALL FAR SECTION:OFFSET (Intel格式)
JMP FAR SECTIOM:OFFSET (Intel格式)
lcall $section, $offset (AT&T格式)
ljmp $section,$offset (AT&T格式)
与之相应的远程返回指令,则为:
RET FAR STACK_ADJUST(Intel格式)
lret $stack_adjust (AT&T格式) (8) 间接寻址的一般格式,两者区别如下:
SECTION: [BASE + INDEX*SCALE + DISP](Intel格式)
section: disp (base, index, scale)(AT&T格式)
注意在AT&T格式中隐含了所进行的计算。例如,当SECTION省略,INDEX和SCALE也省略,BASE为EBP,而DISP(位移)为4时,表示如下:
[ebp-4](Intel格式)
-4(%ebp) (AT&T格式)
在AT&T格式的括号中如果只有一项base,就可以省略逗号,否则不能省略,所以(%ebp)相当+(%ebp,,),进一步相当于(ebp, 0, 0)。又如,当INDEX为EAX, SCALE为4 (32位),DISP为foo,而其他均省略,则表示为:
[foo+EAX*4](Intel格式)
foo(,%EAX,4)(AT&T格式)
这种寻址方式常常用于在数据结构数组中访问特定元素内的一个字段,base为数组的起始地址,scale为每个数组元素的大小,index为下标。如果数组元素是数据结构,则disp为具体字段在结构中的位移。
2 嵌入在C语言中的汇编语言
当需要在C语言的程序中嵌入一段汇编语言程序段时,可以使用gcc提供的“asm”语句功能。其具体格式如下:
__asm__ ("汇编代码段") __asm__ __volatile__ (指定操作 + "汇编代码段")由于具体的汇编语言规则相当复杂,所以我们只关心与内核源代码相关主要规则,并通过几个例子来加以描述,其他规则具体请参考相关CPU的手册。
例1:在include/asm-i386/io.h中有这么一行:
#define __SLOW_DOWN_IO __asm__ __volatile__ ("outb %al, $0x80")
表示8位输出指令。b表示这是8位的,而0x80是常数,即所谓“直接操作数”,所以要加上前缀“$”,而寄存器名al也加了前缀“%”。
例2:在同一个asm语句中也可以插入多行汇编程序。就在同一个文件中,在不同的条件下,__SLOW_DOWN_IO又有不同的定义:
#define __SLOW_DOWN_IO __asm__ __volatile__("/njmp 1f/n1:/tjmp 1f/n1:")
这里就不那么直观了,这里,一种插入了三行汇编语句,“/n”就是换行符,而“/t”则表示TAB符。这些规则跟printf语句中转义字符的规则一样:
l: jmp lf
l:
这里转移指令的目标lf表示前往(f表示forward)找到第一个标号为l的那一行。相应地,如果是lb就表示往后找。所以这一小段代码的用意就在于使CPU空做两条转移指令而消耗一些时间。
例3:下面看一段来自include/asm-i386/atomic.h的代码。
{
__asm__ __volatile__(
LOCK "addl %1,%0"
:"=m" (v->counter)
:"ir" (i), "m" (v->counter));
}
一般而言,往C代码中插入汇编语言的代码是很复杂的,因为这里有个分配寄存器呵与C语言代码中的变量结合的问题。为了这个目的,必须对所使用的汇编语言做更多的扩充,增加对汇编工具的指导作用。
下面,先介绍一下插入C代码中的汇编成分的一般格式,并加以解释。以后在我们碰到具体代码时还会加以提示:
插入C代码中的一个汇编语言代码片断可以分成四部分,以“:”号加以分隔,其一般形式为:
指令部:输出部:输入部:损坏部
注意不要把这些“:”和程序标号中所用的(如前面的1:)混淆。
第一部分就是汇编语句本身,其格式与汇编程序中使用的基本相同,但也有区别,不同支出马上会讲到。这一部分可以称为“指令部”,是必须有的,而其他各部分则可视具体情况而省略,所以最简单的情况下就与常规的汇编语句基本相同,如前面两个例子那样。
在指令部中,数字加上前缀%,如%0、%1等等,表示需要使用寄存器的样板操作数。那么,可以使用此类操作数的总数取决于具体CPU中通用寄存器的数量,这样,指令部中用到了几个不同的操作数,就说明有几个变量需要与寄存器结合,由gcc和gas在编译时根据后面的约束条件变通处理。
那么,怎样表达对变量结合的约束条件呢?这就是其余几个部分的作用。“输出部”,用以规定对输出变量,即目标操作数如何结合的约束条件。必要时输出部中可以有多个约束,以逗号分隔。每个输出约束以“=”号开头,然后时以个字母表示对操作数类型的说明,然后时关于变量结合的约束。例如:
:"=m" (v->counter),这里只有一个约束,“=m”表示相应的目标操作数(指令部中的%0)是一个内存单元
v->counter。凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在实行嵌入汇编代码以后均部保留执行之前的内容,这就给gcc提供了调度使用这些寄存器的依据。
输出部后面是“输入部”。输入约束的格式与输出约束相似,但不带“=”号。在前面例子中的输入部有两个约束。第一个为“ir”(i),表示指令中的%1可以是一个在寄存器中的“直接操作数”,并且该操作数来自于C代码中的变量名i(括号中)。第二个约束为"m" (v->counter),意义与输出约束中相同。
回过头来,我们再来看指令部中的%号加数字,其代表指令的操作数的编号,表示从输出部的第一个约束(序号为0)开始,顺序数下来,每个约束计数一次。
另外,在一些特殊的操作中,对操作数进行字节操作时也允许明确指出是对哪一个字节操作,此时在%与序号之间插入一个”b“表示最低字节,插入一个”h“表示次低字节。
回到上面的例子,读者现在应该很容易理解这段代码的作用是将参数I的值加到v->counter上。代码中的关键字LOCK表示在执行addl指令时要把系统的总线锁住,保证操作的”原子性(atomic)“
例4:再看一段嵌入汇编代码,这一次取自include/asm-i386/bitops.h
#ifdef CONFIG_SMP
#define LOCK_PREFIX "lock ; "
#else
#define LOCK_PREFIX ""
#endif
#define ADDR (*(volatile long *) addr)
static __inline__ void set_bit(int nr, volatile void * addr)
{
__asm__ __volatile__( LOCK_PREFIX
"btsl %1,%0"
:"=m" (ADDR)
:"Ir" (nr));
}
这里的指令btsl将一个32位操作数中的某一位设置成1。参数nr和addr表示将内存地址为addr的32位数的nr位设置成1。
例5:再来看一个复杂,但又非常重要的例子,来自include/asn-i386/string.h:
{
int d0, d1, d2;
__asm__ __volatile__(
"rep ; movsl/n/t"
"testb $2,%b4/n/t"
"je 1f/n/t"
"movsw/n"
"1:/ttestb $1,%b4/n/t"
"je 2f/n/t"
"movsb/n"
"2:"
: "=&c" (d0), "=&D" (d1), "=&S" (d2)
:"0" (n/4), "q" (n),"1" ((long) to),"2" ((long) from)
: "memory");
return (to);
}
这里的__memcpy函数就是我们经常调用的memcpy函数的内核底层实现,用来复制内存空间的内容。参数to是复制的目的地址,from是源地址,n位复制的内容的长度,单位是字节。gcc生成以下代码:
testb $2, %b4
je 1f
movsw
1: testb $1, %b4
je 2f
movsb
2:
其中输出部有三个约束,函数内部变量d0、d1、d2分别对应操作数%0至%2,其中d0必须放在ecx寄存器中;d1必须放在edi寄存器中;d2必须放在esi寄存器中。再看输入部,这里又有四个约束分别对应操作数%3、
%4、%5、%6。其中操作数%3与操作数%0使用同一个寄存器ecx,表示将复制长度从字节个数换算成长字个数(n/4);%4表示n本身,要求任意分配一个寄存器存放;%5、%6即参数to和from,分别与%1和%2使用相同的寄存器(edi和esi)
再看指令部。第一条指令是”rep“,只是一个标号,表示下一条指令movsl要重复执行,每重复一遍就把寄存器ecx中的内容减1,直到变成0为止。所以,在这段代码中一共执行n/4次。movsl是386指令系统中一条很重要的复杂指令,它从esi所指到的地方复制一个长字到edi所指的地方,并使esi和edi分别加4。这样,当代码中的movsl指令执行完毕,准备执行testb指令的时候,所有的长字都复制好了,最多只剩下三个字节了。在这个过程中隐含用到了上述三个寄存器,这就说明了为什么这些操作数必须在输入和输出部中指定必须存放的寄存器。
接着就是处理剩下的字节了(最多三个)。先通过testb测试操作数%4,即复制长度n的最低字节中的bit2,如果这一位位1就说明至少还有两个字节,所以就通过movesw复制一个短字(esi和edi则分别加2),否则就把它跳过。再通过testb测试操作数%4的bit1,如果这一位为1,就说明还剩一个字节,所以通过指令movsb再复制一个字节,否则跳过。当达到标号2的时候,执行就结束了。
在include/asm-i386中有许多最基本的汇编函数,有时间的话,大家不妨随便找几个练习一下。
- Linux内核入门(四)—— 内核汇编语言规则
- Linux内核入门(四)—— 内核汇编语言规则
- Linux内核入门(四)—— 内核汇编语言规则
- Linux内核入门(四)—— 内核汇编语言规则
- Linux内核入门—— 内核汇编语言规则
- Linux内核驱动(四)——内核制作
- Linux内核中的汇编语言
- Linux内核中的汇编语言
- Linux内核中的汇编语言
- Linux内核中的汇编语言
- linux内核中的汇编语言
- linux内核——入门
- Linux驱动移植(四)——Linux内核移植
- Linux内核多线程(四)
- Linux内核多线程(四)
- 【Linux 内核】文件系统(四)
- Linux内核分析(四)
- Linux内核多线程(四)
- JavaFX,Flex和Silverlight横向对比
- 初来此处
- ubuntu 9.04 安装带调试功能的bochs
- CentOS、Ubuntu下安装FreeNX远程桌面
- Linux内核入门(三)—— C语言基本功
- Linux内核入门(四)—— 内核汇编语言规则
- 设计模式-Observer 模式
- 几道经典的SQL笔试题目
- vb标准(四):错误处理
- i q
- 悼江民
- 用批处理打包J2ME程序
- poj 3742
- 说一个道一个.....