《Linux原理与结构》课件第2章_第1页
《Linux原理与结构》课件第2章_第2页
《Linux原理与结构》课件第2章_第3页
《Linux原理与结构》课件第2章_第4页
《Linux原理与结构》课件第2章_第5页
已阅读5页,还剩134页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第二章平 台 与 工 具2.1硬件平台2.2Intel处理器体系结构2.3GNUC语言2.4GNU汇编语言2.5GNU链接脚本2.6常用数据结构如果不考虑虚拟机监控器(VMM),操作系统内核就是最底层的系统软件。操作系统内核直接运行在计算机硬件平台之上,其设计技术与实现方法都与硬件平台有着十分密切的关系。离开了硬件平台的支持,操作系统内核的许多管理工作都难以开展。事实上,计算机硬件平台中的许多功能也是专门为操作系统内核设计的,只有操作系统内核才会使用它们。要了解操作系统内核的原理与结构,就必须了解计算机的硬件平台。

在复杂的计算机硬件平台中,最核心的是处理器,与内核设计关系最密切的也是处理器。虽然Linux内核可以运行在多种处理器之上,但Intel系列的处理器是Linux支持的第一种处理器,也是目前最常见的处理器,更是本书的讨论基础。

Linux内核是用C和汇编语言写成的,然而它所用的C语言经过了GNU的扩展,所用的汇编语言采用的是AT&T的格式。Linux内核的实现充分利用了GNUC和AT&T汇编的扩展特性,与这两种语言的结合极为紧密。GNUC和AT&T格式的汇编是Linux的核心开发工具,也是理解Linux内核源代码的基础。

另外,在Linux内核的诸多数据结构中,最常见的是链表和树。链表和树的实现方式很多,为了避免重复,Linux设计了通用链表和红黑树。当需要将某种结构组织成链表或红黑树时,Linux就会在其中嵌入一个通用链表节点或红黑树节点。

操作系统所管理的计算机硬件平台大致由CPU、内存、外存和其它外部设备组成,它们之间通过总线连接在一起。图2.1是一种抽象的计算机硬件平台的组织结构。2.1硬件平台

图2.1计算机硬件平台的组织结构处理器又叫CPU,是整个计算机系统的大脑,它负责执行由指令构成的程序,并通过程序的执行来控制整个计算机系统。一个计算机系统中可以有一个或多个处理器,一个处理器中又可以有一个或多个核(Core)。为方便起见,可以将一个核看成一个独立的处理器。一个以多核处理器为核心的计算机系统等价于一个多处理器(SMP)系统。

内存是处理器执行程序、加工数据的场所,是处理器可以直接访问的存储空间。内存通常被抽象成一个字节数组,其中的每个字节都有一个地址。处理器可通过地址随机地访问内存中的任意一个字节。为了加快内存的访问速度,计算机系统中通常都提供了一些高速缓存(Cache)。Cache通常由硬件管理。

I/O设备通常由I/O控制器和物理设备组成。处理器通过I/O控制器管理物理设备。对内核来说,I/O控制器主要由控制与状态寄存器(CSR)和数据寄存器组成。处理器通过读CSR获得设备的状态、通过写CSR来控制设备的动作、通过读写数据寄存器与I/O设备交换数据。因而,内核通常将一个I/O设备抽象成一组寄存器,并给每个寄存器一个I/O地址。处理器通过I/O地址访问所有的I/O寄存器。有些处理器还提供了I/O指令,专门用于访问I/O寄存器,如Intel的in、out指令。现代计算机系统中的许多设备寄存器可被映射到物理地址空间中。此时,每个设备寄存器都有一个物理内存地址,处理器可以像访问物理内存一样访问设备的寄存器。这种方式的I/O称为内存映射I/O(MMIO),它的使用更加方便,但会消耗物理地址。

在所有的I/O设备中,对系统影响最大的是外部存储设备,如硬盘、光盘等。操作系统、应用程序、数据文件等都存储在外部存储设备(如磁盘)上。为了便于管理,通常把外存抽象成一个数据块的数组,每个数据块都有一个序号。处理器可以通过序号随机地读、写外存中的任何一个数据块。对外存的操作以块为单位,因此又称外存为块设备。对应地,其它I/O设备称为字符设备。

总线负责将处理器、内存、I/O控制器等连接起来,组合成一个完整的计算机系统。常用的总线有ISA、PCI、PCI-E、AGP、ATA、SCSI等。总线除负责计算机系统中各部件之间的通信之外,还负责检测、枚举连接在其上的设备,报告它们的信息。

在众多的处理器中,最常见的是Intel处理器。Intel处理器是一个大家族,包括多个系列的产品,如80386、80486、Pentium、PentiumII、PentiumIII、Pentium4、Xeon、CoreTMDuo、CoreTMSolo等。若按处理器的体系结构划分,可将主流的Intel处理器分为两大类,即IA-32和Intel64。其中,IA-32提供32位编程环境,Intel64提供64位编程环境。Intel64与IA-32是兼容的。2.2Intel处理器体系结构

Intel处理器为操作系统内核的设计提供了多种支持机制,包括操作模式、内存管理机制、进程管理机制、中断处理机制、保护机制、专用寄存器和指令等。2.2.1处理器操作模式

定义操作模式的目的主要是为了兼容。在设计8086处理器时,Intel并没有定义操作模式,此时的处理器使用20位的物理地址,最多可访问1MB的物理内存,也未对操作系统进行任何保护。当80386出现时,处理器开始使用32位的逻辑地址,而且提供了程序间的隔离与保护,于是引入了保护模式以区分前期的实模式。为了在保护模式中能够运行实模式的程序,又引入了虚拟8086模式。当Intel64出现之后,处理器开始使用64位地址,于是又引入了64位模式以区别于前期的保护模式。为了在64位模式中运行保护模式的程序,又引入了兼容模式。

IA-32体系结构提供了3种操作模式和1种准操作模式。实模式是与8086兼容的操作模式,但有一些扩展。保护模式是处理器的一种最基本的操作模式,在这种模式中,处理器的所有指令以及体系结构的所有特色都是可用的,并且能够达到最高的性能。系统管理模式是一种特殊的操作模式,是提供给操作系统的一种透明管理机制,用于实现电源管理等特殊操作。虚拟8086模式是一个准操作模式,允许处理器在保护模式中执行实模式的程序。

Intel64体系结构又新增了一种IA-32e操作模式,该操作模式又包含两种子模式,即兼容模式和64位模式。当处理器运行在兼容模式时,它可以不加修改地运行大多数IA-32体系结构的程序。当处理器运行在64位模式时,它可以使用64位的线性地址空间和一些新增加的特性。IA-32e不再支持虚拟8086模式。

处理器加电或Reset后的默认操作模式是实模式。操作系统内核的初始化部分负责将处理器由实模式切换到其它模式。实模式和保护模式之间的转换由控制寄存器CR0中的PE位控制;保护模式与IA-32e模式之间的转换由IA32_EFER寄存器中的LME和CR0中的PG位控制;兼容模式与64位模式之间的转换由代码段寄存器CS中的L位控制;保护模式和虚拟8086模式之间的转换由标志寄存器EFLAGS中的VM位控制。

进入系统管理模式的唯一途径是SMI中断。在系统管理模式中执行指令RSM会将处理器切换回原来的操作模式。操作模式之间的转换关系如图2.2所示。

图2.2处理器操作模式之间的转换2.2.2段页式内存管理

IA-32体系结构提供了极为复杂的段页式内存管理机制,即先分段,再分页。其中段式管理是默认、必须的,页式管理是可选的。保留段式是为了兼容,提供页式是为了支持虚拟内存。

在段式管理中,处理器可寻址的线性内存空间被划分成了若干个大小不等的段。一个段就是线性地址空间中的一个连续的区间。段中可保存代码、数据、堆栈或其它系统级的数据结构。段的属性信息由与之对应的段描述符描述。段描述符是一个数据结构,其一般格式如图2.3所示。

图2.3段描述符结构描述符中的第2、3、4、7字节组成了段的基地址(Baseaddress),用于定义段在线性地址空间中的开始位置。基地址可以在0~4GB之间浮动。

描述符中的第0、1字节和6字节的低4位(共20位)组成了段界限(Segmentlimit),用于定义段的长度。长度的单位由粒度(G)位表示。当G为0时,段以字节为单位,最大段长为1MB;当G为1时,段以页(4KB)为单位,最大段长为4GB。

DPL是段的特权级,其值在0~3之间。

S是系统标志,用于区分段的类别,0表示系统段,1表示用户段。

Type是段的类型。对系统段,类型域由4位组成,可表示16个系统段类型之一,如2表示LDT、9表示32位有效TSS、B表示32位忙TSS、E表示中断门、F表示陷阱门等。对用户段,类型域中的4位(0~3,3为高位)被重新解释如下:

(1)第3位为0表示数据段。此时,第2位表示地址扩展方向(0表示向大方向扩展,1表示向小方向扩展),第1位表示段是否可写(0表示不能写,1表示可写)。

(2)第3位为1表示代码段。此时,第2位是相容标志(0表示非相容,1表示相容),第1位是可读位(表示代码段是否允许读,1表示允许,0表示不允许)。保护模式的代码段不允许写。

(3)第0位是存取位,0表示段尚未被存取过,1表示段已被存取过。

D/B标志表示有效地址和操作数的长度。

L标志仅出现在IA-32e模式的代码段描述符中,用于表示执行该段代码时处理器的操作子模式,1表示64位模式,0表示兼容模式。

堆栈段通常是向下扩展的、可读写的数据段。

按照Intel的设想,每个进程(Intel称任务)都可以定义自己的代码段、数据段等,而每个段都需要描述符,因而系统中会有许多描述符。为了便于管理,Intel用段描述符表来组织系统中的描述符。段描述符表是一个段描述符的数组,大小可变,最大可达64KB,最多可保存8192个8字节的段描述符。段描述符表又分为两大类,即全局描述符表和局部描述符表。全局描述符表(GDT)中的描述符是全局共用的,其中的第0个描述符保留不用(全为0)。在系统进入保护模式之前必须为其定义一个GDT。由于GDT本身仅是线性地址空间中的一个数据结构,没有对应的描述符,因而IA-32体系结构专门定义了GDTR寄存器来存放当前GDT的信息。

与GDT不同,局部描述符表(LDT)是系统段,其中可存放局部的段描述符,如进程自己的代码段、数据段等。定义LDT的描述符叫LDT描述符,出现在GDT中。IA-32体系结构专门提供了一个LDTR寄存器,用于保存当前使用的LDT的信息。有了描述符表之后,可以用描述符在GDT或LDT中的索引来标识它,这种标识称为段选择符。段选择符是16位的标识符,它的第3~15位是索引,表示描述符在GDT或LDT中的位置;第2位是指示器(TI),表示索引所对应的描述符表(0表示GDT,1表示LDT);第0、1两位是请求特权级RPL。

一个段选择符加上一个偏移量可以唯一地标识一个逻辑地址。逻辑地址是程序中使用的地址,不是线性地址,也不是物理地址,在使用之前必须对其进行转换。转换的过程是:以段选择符为索引查描述符表,获得段的描述符,从中取出段的基地址,将其加上偏移量即可得到与之对应的线性地址,如图2.4所示。图2.4逻辑地址到线性地址的转换如果没有启动分页机制,经过段描述符转换后的线性地址就是物理地址。

逻辑地址的转换极为频繁,应该将最近使用的描述符缓存起来,以加快地址转换的速度。为此,IA-32体系结构提供了6个段寄存器,即CS、SS、DS、ES、FS和GS,每个段寄存器中可以缓存一个段描述符。有了段寄存器后,在使用一个段之前,可以先将它的描述符装入到一个段寄存器中。如此以来,逻辑地址就变成了段寄存器+偏移量,逻辑地址到线性地址的转换可以在处理器中完成,不需要再查描述符表。虽然以段为单位可以进行内存管理,但这种方法比较笨拙,而且与现代虚拟内存管理的思想不符,因而IA-32体系结构允许对段内的内存进行再分页,即在段式管理的基础上再增加一套页式管理,也就是段页式管理。

在段页式管理中,段内的线性地址空间被分割成大小相等的线性页(4KB、

4MB或2MB等),物理内存空间也被分成同样大小的物理页。操作系统内核维护一个页表,用于管理线性页到物理页的映射。在页表中,一个线性页可以有对应的物理页,此时利用页表可以完成线性地址到物理地址的转换。一个线性页也可以没有对应的物理页,此时的线性地址无法直接转换成物理地址,处理器会产生一个fault异常。操作系统内核在处理这种异常时,可以临时为线性页分配物理页并在其中填入适当的内容。异常处理之后,线性地址即可以转换成物理地址。

页表可能很大,因而又被分成多级,如IA-32体系结构将它的页表分成两级,即页目录和页表。页目录是一个数组,其元素叫页目录项(PDE),每个页目录项描述一个页表。页目录的大小为一页(4KB),页目录项的大小为4字节,所以一个页目录中有1024个页目录项,最多可描述1024个页表。页表也是一个数组,其元素叫页表项(PTE),每个页表项描述一个线性页。页表大小为一页(4KB),页表项的大小为4字节,所以一个页表最多可描述1024个线性页。

由于物理页是预先划分好的,其开始位置一定在4KB的边界上,因而页目录和页表项的低12位肯定是0,可以用它们存储页表或页的管理控制信息,如是否有对应的物理页、是否可写等。页目录项与页表项的结构基本相同。图2.5是页表项的结构。图2.5页表项的结构在页表项结构中,P是存在位,表示它所描述的页表或页目前是否在物理内存中,1表示在内存,0表示不在内存;R/W是读写标志位,表示页表或页是否允许写,0表示只读,1表示可读可写;U/S是用户标志位,表示页表或页的特权级,0表示超级用户(特权级为0、1、2),1表示普通用户(特权级为3);A是存取标志位,表示页表或页有没有被存取(读、写)过,当页表或页被存取时,处理器自动设置该标志;D是脏标志位,表示页是否被修改过,当页被修改时,处理器自动设置该标志。

在页目录项中,PAT标志变成了PS标志,表示物理页的尺寸。线性地址到物理地址转换的过程是:按照页表层次将线性地址划分成多个片段;从最高片段开始,以片段值为索引逐个查对应的页表,获得下一级页表的位置;查最后一级页表,获得物理页的位置;将物理页的开始地址加上最后一个片段的值(偏移量)得到的就是线性地址对应的物理地址。图2.6是利用二级页表进行地址转换的过程。图2.6线性地址到物理地址的转换(4KB页)在做线性地址到物理地址的转换时,必须知道所用页目录的位置。IA-32体系结构专门提供了一个CR3寄存器,用于存放当前使用的页目录的物理地址,因此CR3又叫页目录基地址寄存器(PDBR)。在启动分页机制之前,必须定义好页目录并将其基地址装入到CR3寄存器中。只要进程在活动,它的页目录就应该一直驻留在内存。

当然,页目录项也可以直接指向物理页,此时的页大小是4MB。采用4MB页可加快地址转换的速度,因而通常将操作系统内核所占用的页设为4MB页。当页目录项的PS位为1时,它所描述的是一个4MB页而不再是一个页表。

页式管理机制是由操作系统内核启动的,启动的方法是将CR0中的PG标志置1。启动分页机制之后,每个线性地址都需要经过页目录和页表的转换,这显然会大大降低内存访问的速度。为解决这一问题,IA-32体系结构在其处理器芯片中增加了一个高速缓存TLB(TranslationLookasideBuffers),在其中存储最近使用的页目录和页表项。地址转换时,首先查TLB,如其中有缓存的页表项,可立刻进行地址转换;只有当TLB中没有对应的页表项时,才会访问页目录和页表。新访问的页表项会被自动加入TLB。

TLB的内容必须经常刷新以保持与页目录和页表的一致。刷新工作由操作系统内核负责。当页目录或页表项改变时,内核必须立刻使TLB中的相应项失效。特别地,当CR3改变时,TLB中的所有内容(Global页除外)会自动失效。INVLPG指令可以将TLB中的指定项设为无效。2.2.3内存管理的变化与扩展

段页式内存管理是Intel处理器提供的最基础的内存管理机制,在此基础上,Intel还提供了许多变化与扩展。

(1)基本平板式内存管理。基本思路是屏蔽掉段式管理,完全采用页式管理。做法是定义一个代码段、一个数据段,两个段的基地址都是0,大小都是4GB。如此以来,逻辑地址就是线性地址,段式管理的作用被屏蔽。对段内内存(实际是所有内存)的管理完全依靠分页机制。

(2)保护平板式内存管理。基本思路是屏蔽掉段式管理,但保留一些保护特征,主要采用页式管理。一种典型的做法是定义四个段,即内核代码段、内核数据段、用户代码段、用户数据段,四个段的基地址都是0,大小都是4GB,不同的是内核段的特权级是0,用户段的特权级是3。将操作系统内核的代码和数据都放在内核段中,将所有用户的代码和数据都放在用户段中(不做区分)。进程执行用户代码时使用用户段,执行内核代码时使用内核段。段的地址转化作用被屏蔽,但基于特权级的保护特征被保留,如用户代码不能访问内核数据等。段内内存用页式管理。一般情况下,每个进程都会用到四个段,因而需要为每个进程定义四套页目录/页表。但对同一个进程来说,由于它对四个段的使用绝对不会重叠,因而可以将四个段叠加起来,看成是进程的平板地址空间,同时也可将四套页目录/页表合并为一套。在合并后的页目录/页表中,页表项可能代表不同段中的页。页表项中的U/S标志用于区分用户页和内核页。如果操作系统内核能保证各进程的页目录/页表中没有重叠的表项,就可以保证进程之间的相互隔离。

(3)多段式内存管理。基本思路是完全采用段式管理,屏蔽掉页式管理。

(4)基于物理地址扩展的页式内存管理。从PentiumPro开始,IA-32体系结构引入了物理地址扩展(PAE)机制以支持36位物理地址。在该管理模式中,处理器可访问的物理地址空间被扩充到了64GB,但线性地址空间仍然为4GB,然而一个线性页可以映射到任意一个物理页。为做到这一点,页目录和页表项被扩充到了64位,因而一个页目录或页表中的项数变成了512,一个页目录仅能描述1GB的线性地址空间,4GB的线性地址空间需要4个页目录描述。新引入一个仅有4个表项的页目录指针表PDP(Directory-PointerTable),其中的每个表项指向一个页目录(一个PDP可以描述4GB的空间),CR3指向PDP。地址转换机制被修改,以便将32位线性地址翻译成36位物理地址。当页目录项中的PS位被置1时,它描述的页变成了2MB页。

(5)

64位平板内存管理。在64位模式中,段通常被关闭,虽然不是完全关闭。处理器将CS、DS、ES、SS的基地址统统看成0,而且不再做段界限检查,因而逻辑地址就是线性地址。但FS和GS的基地址可以不是0。如果FS、GS的基地址不是0,在将逻辑地址转换成线性地址时要加上FS、GS的基地址。值得注意的是,FS、GS的基地址是64位地址(兼容模式只用它的低32位),记录在MSR中。在64位模式中,对内存的管理完全依靠分页机制。Intel64体系结构扩展了PAE机制,使之能支持64位线性地址和52位物理地址。主要的扩展包括:页目录指针表(PDP)被扩充到了512项;新引入一个第四级页映射表PML4(PageMapLevel4Table),它的每个表项可指向一个PDP;所有四级页表的表项都被扩充到了64位(PAE必须使能);页目录项中的PS标志用于控制4KB和2MB页;CR3指向PML4;在所有页表项的第63位上新增了一个执行禁止标志EXB(Execute-DisableBit),如果该标志被置1,它对应的页只能用做数据页,不能用做代码页,即其上的代码被禁止执行。2.2.4内存保护

一旦保护机制被启动,处理器就会对每一次内存访问进行保护性检查,以确保所有的访问都满足保护策略。当发现违反内存保护约定的内存访问时,处理器会产生异常。由于保护检查和地址转换是并行进行的,因而检查本身并不会带来额外的开销。

保护检查包括段级检查和页级检查两种,检查的依据是段描述符、页目录和页表,检查的顺序是先段后页,检查的基础是特权级。特权级是Intel为实现保护而定义的特权编号,从0到3,其中0是最高特权级,3是最低特权级。系统中每个段(代码段、数据段、堆栈段等)都有特权级,因而系统中所有的程序与所有的数据也都有特权级。

段一级的检查包括段界限检查、段类型检查、特权级检查、长指针检查等。段一级检查的原则是:

(1)低特权级的代码不能访问高特权级的数据。

(2)高特权级的代码可以访问低特权级的数据。

(3)代码只能使用与其特权级相同的堆栈,当特权级切换时,堆栈也要随之切换。

(4)只能向具有相同特权级的非相容代码段转移控制(长JMP和长CALL)。

(5)可以向具有同等或较高特权级的相容代码段转移控制,但不能向具有较低特权级的相容代码段转移控制(长JMP和长CALL)。

(6)即使使用调用门、中断门或陷阱门,也不能从高特权级向低特权级转移控制。

(7)不允许用长RET向高特权级转移控制。页一级的检查包括特权级检查和读写检查,相关的标志是页目录/页表项中的U/S和R/W位。U/S为0的页是超级页,U/S为1的页是用户页。一般情况下,超级页中的代码可以访问所有的页(不管R/W标志),用户页中的代码只能访问用户页。当CR0.WP被置1时,超级页中的代码也不能写只读的用户页。

在Intel64体系结构中,新引入了执行禁止标志NXB,用于防止缓冲区溢出之类的攻击。将IA32_EFER.NXE置1可使能页级执行检查,此后NXB被置1的页仅能用作数据页,试图执行数据页中的指令会引起处理器异常。2.2.5进程管理

当处理器运行在保护模式时,它的所有工作都是在任务进程中完成的,因而至少需定义一个任务。任务由执行空间和任务状态段组成。执行空间由代码段、堆栈段和数据段表示,它们的描述符可直接放在GDT或任务的LDT中。任务状态段(TSS)用于记录任务的状态,如通用寄存器及EFAGS、EIP、CR3、LDTR、TR寄存器的状态,0、1、2级堆栈的栈底指针等。一个TSS可唯一地描述一个任务,如图2.7所示。

图2.732位任务状态段(TSS)在IA-32e模式中,TSS被大大精简,仅剩余了三个栈底指针和一个I/O许可位图,但新增加了一个中断栈表(7个栈指针)。

任务状态段由专门的TSS描述符描述。当前任务的TSS描述符被记录在专门的任务寄存器(TR)中。通过TSS描述符或任务门,利用CALL、JMP等指令可自动完成任务切换,但代价很高。现代操作系统内核都选用代价更低的切换方法,如通过指令保存和恢复必要的寄存器从而完成任务切换。64位模式已不再支持自动任务切换。由于CR3在TSS中,因而当进程切换时,页目录也会随着切换,也就是说,每个进程都可以有自己独立的线性地址空间。但为了系统能够正常运行,在任何时候处理器都应该能够访问到所有进程的TSS,即应该将TSS保存在所有进程都可访问到的共享地址空间中。进一步地,GDT、IDT、操作系统内核代码和系统管理信息等都应该保存在共享地址空间中。事实上,在所有进程的页目录中,确实存在着共用的页表,这些共用的页表描述的就是进程间共享的线性地址空间。2.2.6中断处理

Intel的中断是外部中断、异常和陷入的统称。外部中断来自处理器之外的硬件,如外设,是随机的;异常来自于处理器内部,表示在处理器执行指令的过程中检测到了某种错误条件(如被0除、段越界等);陷入来自程序,由INTn、INTO等指令产生。外部中断可以被屏蔽,但陷入和异常不能被屏蔽。屏蔽中断的方法是清除EFLAGS寄存器中的IF标志。中断的产生表示系统中出现了某种必须引起处理器关注的事件,处理器需要立刻离开当前的工作转去处理这些事件。处理中断的程序称为中断处理器程序,处理异常的程序称为异常处理程序,处理陷入的程序称为系统调用服务程序。处理程序可位于内核空间的任意位置,且可有不同的特权级,因而需要专门的数据结构来描述它们。Intel处理器称处理程序的入口为门(Gate)。可用的门有三类,分别是中断门、陷阱门和任务门(Linux只用到了中断门和陷阱门)。中断门和陷阱门是进入中断和异常处理程序的门户,分别由中断门和陷阱门描述符定义。中断门描述符的格式如图2.8所示。图2.8中断门描述符陷阱门描述符与中断门描述符的格式基本一致,所不同的是陷阱门描述符中的类型是“D111”。类型中的D表示位数,0为16位,1为32位。在两种门描述符中,选择符与偏移量合起来定义了一个处理器程序的入口地址,DPL定义了门的特权级。

中断门与陷阱门的功能也基本一致,都定义了一个处理程序的入口地址,所不同的是处理IF标志的方式。当通过中断门进入处理程序时,IF标志被清掉(中断被关闭);当通过陷阱门进入处理程序时,IF标志保持不变。为了处理中断,Intel处理器给它的每个中断和异常都赋予了一个中断向量号,并定义一个中断描述符表(IDT)用于建立中断向量号和门之间的对应关系。

Intel处理器定义的中断向量号共256个,其中0~31被处理器保留,主要用于异常和不可屏蔽中断(NMI),32~255可由操作系统内核自由使用,如赋给外设等。

IDT是一个描述符数组,由一组门描述符组成,每一个中断向量号对应其中的一个门描述符。因为只有256个中断和异常,所以IDT只有256项。与GDT相同,IDT也不是一个段,没有对应的段描述符。IDT可以驻留在线性地址空间的任何位置。Intel处理器专门提供了一个IDTR寄存器来记录IDT的基地址和界限信息。当中断或异常发生时,处理器以中断向量号为索引查IDT可得到与之对应的门描述符,从而可得到处理程序的入口地址。图2.9是IDTR和IDT之间关系。图2.9IDTR与IDT之间的关系外部中断被处理完后,处理器会接着执行被中断指令的下一条指令。陷入指令被处理完后,处理器会接着执行陷入指令的下一条指令。异常处理程序被执行完后,处理器的返回位置依赖于异常的类型。Intel处理器目前定义了三类共20个异常。故障(Fault)类异常是可以更正的,当故障类异常被处理完后,处理器会重新执行产生故障的指令。陷阱(Trap)类异常是由特殊的陷入指令(INT3、INTO)引发的,当陷阱类异常被处理完后,处理器会接着执行陷入指令的下一条指令。中止(Abort)类异常是严重的错误,处理器无法保证程序能够继续正常执行。

Intel规定,通过中断门或陷阱门只能向同级或更高特权级的代码段转移控制,因而通常将处理程序定义在0特权级的代码段(内核代码段)中。

通过陷入指令也可以进入中断或异常处理程序,但要进行特权级检查,要求进入前的特权级(CPL)必须小于或等于门描述符的特权级(DPL)。中断或陷阱门的DPL通常被设为0,因而用户程序无法通过INTn指令直接进入中断或异常处理程序。中断或异常处理程序运行在当前进程的上下文中,但可能使用不同的堆栈。如果中断或异常发生时处理器在第0特权级上(在执行内核),则处理程序可直接使用当前进程的系统堆栈,不用切换堆栈。如果中断或异常发生时处理器在第3特权级上(在执行用户程序),则需要切换堆栈,即从当前进程的用户堆栈切换到它的系统堆栈。TR中记录着当前进程的TSS,其中包含当前进程的系统堆栈的栈底。

中断或异常发生时,处理器会自动在栈顶压入一些参数,其中EFLAGS是中断或异常发生前的系统状态,SS:ESP是中断或异常发生前的用户堆栈栈顶,CS:EIP是中断或异常的返回地址。只有发生堆栈切换时,才会在栈顶压入SS:ESP。

仅有一些特殊的异常会在栈顶压入error-code(见表3.3),外部中断和陷入不会自动在栈顶压入error-code。为了使堆栈保持平衡,对不自动压入error-code的中断和异常,处理程序应在栈顶压入一个值,如Linux的外部中断处理程序会压入中断向量号、无error-code的异常处理程序会压入0或-1。图2.10是中断发生时堆栈的变化情况。图2.10中断或异常发生时的堆栈变化在64位模式中,中断和异常的处理方式有所改变,如处理程序必须在64位代码段中,因而中断和陷阱门描述符被扩充到了16字节,其中的偏移量被扩充到了64位;IDT中仅有新格式的门描述符;堆栈宽度变成了64位,而且当中断发生时,会无条件地压入栈指针(SS:RSP);当需要切换堆栈时,新的SS被强制设为NULL;新增加了的中断堆栈表(IST)机制,允许为特定的中断或异常指定专门的堆栈。2.2.7APIC

高级可编程中断控制器(AdvancedProgrammableInterruptController,APIC)是老式8259中断控制器(PIC)的升级产品,APIC本身的升级版分别叫做xAPIC和x2APIC。

APIC采用分布式体系结构,由LocalAPIC和I/OAPIC通过专用总线或系统总线互连构成,如图2.11所示。图2.11APIC结构

I/OAPIC接收来自设备的中断,把它们传递给选定的一个或一组LocalAPIC。LocalAPIC可以接收外部的(来自I/OAPIC或8259A)、内部的(来自LocalAPIC的内部时钟等)或来自其它处理器的(IPI)中断,并把它们传递给处理器核。LocalAPIC通常被集成在处理器核中。每个LocalAPIC有一个唯一的标识符(ID),用于标识LocalAPIC,也用于标识与之关联的处理器核。

LocalAPIC的内部中断通常有5~6个,包括LocalAPIC定时器、温度传感器、性能计数器、内部错误、LINT0和LINT1等,其中LINT0和LINT1是两个管脚,用于连接其它中断源,如8259PIC。LocalAPIC提供了一个局部中断向量表,用于设定各个内部中断的向量号、递交方式等。可以将LINT0或LINT1的中断递交方式设为ExtINT,此时处理器认为该管脚上连接的是PIC,会按通常的应答方式获取中断向量号。

对操作系统内核来说,LocalAPIC由一组寄存器构成,包括LocalAPICID、LocalAPICVersion、TaskPriorityRegister(TPR)、ProcessorPriorityRegister(PPR)、In-ServiceRegister(ISR)、InterruptRequestRegister(IRR)、InterruptCommandRegister(ICR)、LocalVectorTable(LVT)、EndofInterruptRegister等。这些寄存器被映射到处理器物理地址空间中的一个4KB大小的区域内,缺省的起始地址为0xFEE00000。操作系统内核可以将APIC的寄存器重新映射到物理地址空间的其它区域。LocalAPIC的状态和寄存器基地址记录在MSR寄存器IA32_APIC_BASE中。

多核处理器系统中有多个LocalAPIC,它们的寄存器都被映射到相同的位置。每一个逻辑处理器都可以访问该映射页,但访问的结果各不相同。当逻辑处理器读写LocalAPIC映射页时,它实际上访问的是自己的LocalAPIC。

I/OAPIC也由一组寄存器组成,包括I/OAPICID、I/OAPICVER、I/OAPICARB、I/OREDTBL等。其中I/OREDTBL是一个中断重定向表,用于确定各外部中断的递交目的地、向量号、递交模式等。与LocalAPIC的寄存器不同,I/OAPIC中的寄存器只能用间接方法访问,方法是先将要访问寄存器的偏移量写入选择寄存器IOREGSEL,而后再读或写数据寄存器IOWIN。

每一个I/OAPIC都有一个物理基地址ioapicaddr,这一地址实际就是该I/OAPIC中寄存器IOREGSEL的物理地址,寄存器IOWIN的物理地址是ioapicaddr+0x10。缺省情况下,ioapicaddr是0xFEC00000。查ACPI或MP表可获得各I/OAPIC的基地址。2.2.8处理器初始化

从P6系列开始,在IA-32体系结构中增加了一个多处理器初始化协议(MP),用于规定多处理器系统的初始化过程。MP协议将处理器分为自举处理器BSP(BootstrapProcessor)和应用处理器AP(ApplicationProcessor)。在系统加电或Reset之后,多处理器系统中的系统硬件会动态地选举出一个处理器为BSP,其余处理器为AP。

MP协议仅在加电或Reset之后执行一次,此后的INIT不会再执行MP协议。MP协议规定的处理器初始化过程如下:

(1)根据系统拓扑结构,给系统总线上的每一个逻辑处理器一个唯一的8位APICID。该ID号被写入处理器的局部APICID寄存器中。

(2)根据APICID为每个逻辑处理器赋予一个唯一的仲裁优先级。

(3)各逻辑处理器同时执行自己的内建自检代码BIST。

(4)BIST执行完毕之后,系统总线上的各逻辑处理器利用硬件定义的选择机制选举出BSP和AP。而后,BSP开始执行BIOS代码,各AP进入等待状态。

(5)

BSP创建一个ACPI表和一个MP表,并将它自己的APICID填入其中。

(6)在自举程序执行完后,BSP将处理器计数器设置为1,而后向所有的AP广播SISP消息。SISP消息中包含一个向量,指出各AP开始执行的初始化代码的位置。

(7)AP申请初始化信号量,在获得信号量后开始执行初始化代码,将自己的APICID填入ACPI表和MP表,将处理器计数器加1。在初始化代码执行完毕之后,AP执行CLI和HLT指令进入停止状态。

(8)在所有AP都执行完初始化代码之后,BSP通过处理器计数器获得连接在系统总线上的逻辑处理器数,而后执行进一步的自举和启动代码,如内核初始化代码等。

(9)在BSP执行内核初始化代码期间,各AP一直处于停止状态,等待被BSP的处理器间中断信号IPI唤醒。2.2.9寄存器与特权指令IA-32体系结构提供了8个32位的通用寄存器,分别称为EAX、EBX、ECX、EDX、ESI、EDI、ESP、EBP。Intel64体系结构将这8个通用寄存器扩充到了64位,分别称为RAX、RBX、RCX、RDX、RSI、RDI、RSP、RBP,并另外引入了8个通用寄存器,分别称为R8、R9、R10、R11、R12、R13、R14、R15。

IA-32体系结构提供了6个段寄存器,即CS、DS、SS、ES、FS、GS。在64模式中,DS、ES、SS已不再使用,FS、GS用于段重载(影响段的基地址),CS用于控制64位模式与兼容模式的切换。在IA-32体系结构中,指令寄存器是EIP,长度为32位,记录下一条要执行的指令地址。在Intel64体系结构中,指令寄存器被扩充到了64位,称为RIP。

IA-32体系结构提供了一个32位的标志寄存器EFLAGS,用于存放处理器的状态信息(如ZF、CF、OF等)和一些系统控制信息(如IF、IOPL、VM等)。在64位模式中,标志寄存器被扩充到了64位,称为RFLAGS,但内容并未扩展。

IA-32体系结构提供了4个内存管理寄存器,分别称为GDTR、LDTR、IDTR、TR。Intel64体系结构将它们的基地址部分扩充到了64位。

IA-32体系结构提供了5个32位的控制寄存器,分别称为CR0、CR1、CR2、CR3、CR4。其中CR0中包含系统控制标志,用于控制处理器的操作模式和状态,如是否启用分页机制等;CR1保留未用;CR2用于暂存引起页故障异常(fault)的线性地址;CR3中暂存当前使用的页目录的物理基地址;CR4中包含一组体系结构扩展标志,如PAE、PSE等。Intel64体系结构将控制寄存器扩充到了64位,新增加了两个标志位,并引入了一个新的控制寄存器CR8,用于记录任务的优先级。

Intel处理器中还提供了一组专门给操作系统内核使用的、与处理器型号相关的专用寄存器(MSR),用来控制处理器的debug扩展、性能监视、机器检查结构、内存类型范围等。在不同的处理器中,MSR寄存器的个数和功能可能会有所变化。

Intel处理器还提供了8个调试寄存器(DR0-DR7),用于帮助Debug程序设置断点。除了上述寄存器之外,Intel处理器还专门提供了一组系统指令,用来处理系统级的工作。如装入系统寄存器、管理Cache、管理中断、设置Debug寄存器等。有些系统指令只能由操作系统内核执行(要求特权级为0),另一些系统指令可在任意特权级下执行。表2.1中是常用的几条系统指令。

表2.1常用的系统指令

GNU的C语言是对标准C语言的扩展,其编译器称为GCC。GCC是RichardStallman于1984年发起的一个项目,最初的目的是开发一个免费的

C

编译器,因而早期的意思是GNUCCompiler。由于GCC具有灵活的架构并采取了开源策略,因而发布之后迅速被接受、移植、扩充,目前已可支持C、C++、Objective-C、Objective-C++、Java、Fortran、Ada等多种语言,支持30多种处理器家族,可在超过60种平台上运行。现在GCC的意思变成了GNUCompilerCollection。2.3GNUC语言

GCC中的C编译器是Linux的基础,Linux内核源代码只能用GCC编译。GCC支持C语言标准C89、C90、C94、C95、C99等,并有自己的扩展,如:

(1)允许将一个复合语句括在一对圆括号内作为一个表达式使用,如:

#definemaxint(a,b)({int_a=(a),_b=(b);_a>_b?_a:_b;})

(2)允许在一个函数内部定义嵌入式函数,如:

foo(doublea,doubleb){

double square(doublez){returnz*z;}

return square(a)+square(b);

}

(3)可以直接将typeof作数据类型使用,如:

typeof(*x)y[4]; //指针数组,指针所指类型是参数x的类型

typedef typeof(expr)T; //T是expr的类型

(4)可以忽略条件表达式的中间项。如x?:y等价于x?x:y。

(5)允许用longlongint声明64位长整数,用unsignedlonglongint声明64位无符号长整数。后缀LL表示64位整常量,ULL表示64位无符号整常量。

(6)允许声明长度为0的数组。0长度数组通常作为结构的最后一个成员,用于表示一组变长的对象。0长度数组不占用结构的空间,如:

structline{

int length;

char contents[0];

};

(7)允许在宏定义中使用可变数目的参数,如:

#definedebug(format,args...)fprintf(stderr,format,args)

(8)允许在数组、结构、联合等类型变量的声明中使用指示初始化,如:

structpointp={.y=yvalue,.x=xvalue};

仅给x和y成员初值,未明确声明成员的初值是0。

(9)在定义函数时,允许用关键字_attribute_声明属性。可以声明的属性包括对齐要求(aligned)、即将过时(deprecated)、快速调用(fastcall)、无返回(noreturn)、两次返回(returns_twice)、函数所在节(section)、用寄存器传递参数的个数(regparm)等。如:

voidfatal() _attribute_((noreturn));

#define_init _attribute_((_section_(".init.text")))

#defineasmlinkage _attribute_((regparm(0)))第一个声明表示函数fatal不返回,宏_ini表示将函数代码放在.init.text节中,宏asmlinkage表示函数不用寄存器传递参数,所有的参数都在堆栈中传递。

C语言函数之间通常用寄存器传递参数,regparm(n)表示用寄存器传递n个参数,前三个分别在EAX、EBX、ECX寄存器中。

(10)在定义结构或联合类型时,可以通过关键字_attribute_声明相关的属性。在声明变量或结构成员时,可以通过关键字_attribute_声明相关的属性,如对齐要求、存放变量的节等。

(11)在内嵌式汇编程序中,允许用C语言表达式做指令的操作数,不用关心数据的存储位置(见2.4.3节)。

(12)提供了数百个内建函数(多以_builtin_开头),用于实现内存原子存取、对象大小检查、程序优化等,如:

#definelikely(x) _builtin_expect(!!(x),1) //x的预期值是真

#defineunlikely(x) _builtin_expect(!!(x),0) //x的预期值是假

语句if(unlikely(exp){…} 表示exp很少为真,编译器可据此进行优化。

Linux内核中的绝大部分代码是用GNU的C语言写成的,但也包含一些汇编程序。Linux中的汇编程序可以以.S文件的形式单独出现,也可以嵌入在C代码中。Linux中的汇编程序采用GNU的汇编格式,语法上符合AT&T规范。2.4GNU汇编语言2.4.1GNU汇编格式

GNU汇编程序由汇编指令、汇编指示、符号、常量、表达式、注释等组成。为了便于管理和链接,通常将目标程序中的代码、数据等组织成不同的节(section)。节是具有相同属性(如只读)的一段连续的地址区间(在节中使用相对地址)。汇编器根据源程序中的汇编指示将源程序转变成由若干个节组成的目标文件,链接器负责将所有目标文件中相同节的内容拼接起来形成可执行文件。一般情况下,汇编器生成的目标文件中至少包含三个节,分别是.text(只读的程序代码节)、.data(可读写的带初值的变量节)和.bss(可读写的未初始化的变量节)。用户可以通过汇编指示.section声明其它的节,以便细化管理。如在Linux源代码中经常会看到下列格式的程序片段:

1: asminstruction //该指令在.text节中,它的执行可能会出错

.section_ex_table,"a" //切换到_ex_table节,在其中增加一对数据

.align4 //4对齐

.long1b,syscall_fault //异常处理表项,意思是当1处的指令出错时,转syscall_fault

.previous //切换回.text节

另外,在节中还可以再定义子节,以便更好地组织分散在不同源程序中的、属于同一个节的代码和数据。子节的标识是节名后加一个数字编号,如.text.2,编号可以从0到8191。在目标文件中,一个节的内容就是它的各子节内容的总和(按编号排序),但已没有子节的概念。链接器只能看到节,已看不到子节。如未为节定义子节,汇编器会假定其中只有一个编号为0的子节。可以用汇编指示.subsection切换子节,也可以用子节的标识符切换子节。在GNU汇编程序中,常量是一个数字,其值是已知的,可直接使用。常量包括字符、字符串、整数(二进制、八进制、十进制、十六进制等)、大整数(超过32位)和浮点数。符号是由字母、数字、‘_’、‘.’、‘$’等组成的字符串,必须以字母开头,大小写有区别。符号用来给汇编程序中的对象命名,如标号、常量等。符号有两个属性,分别是value和type。可以用‘=’或“.set”改变符号的值。‘.’是一个特殊的符号,表示当前地址。表达式是由操作符和括号连接起来的一组符号和常量,其结果是一个地址或一个数值。GNU汇编的表达式与C语言表达式基本一致。

GNU的汇编指示(AssemblerDirectives)又称伪操作(PseudoOps),是汇编程序提供给汇编器的指示或命令,用于声明目标文件的生成方法。所有的汇编指示都以‘.’开头。GNU汇编提供了100多个指示,在Linux源代码中用到的有以下几种:

(1)

.align

abs-expr1,

abs-expr2,

abs-expr3:用第2个表达式的值填充目标文件中的当前节,使下一个可用位置是第1表达式的倍数允许跳过的最大字节数由第3个表达式决定。第2、3表达式都是可选的。

(2)

.balign[wl]

abs-expr1,

abs-expr2,

abs-expr3:.balign与.align的意思相同。变体.balignw表示填充值是2字节的字,.balignl表示填充值是4字节的长字。

(3)

.p2align[wl]

abs-expr1,

abs-expr2,

abs-expr3:.p2align[wl]与.align[wl]相似,不同之处在于下一个可用位置是2

abs-expr1的倍数。

(4)

.org

new-lc,fill:从new-lc标识的新位置开始存放下面的代码或数据,空出来的空间用fill填充。新位置是在同一节中的偏移量。

(5)

.ascii"string"...:定义一到多个字符串。字符串后不自动加0结尾。

(6)

.asciz"string"...:定义一到多个字符串。字符串后自动以0结尾。

(7)

.string

"str":将字符串拷贝到目标文件中,串以0结尾。

(8)

.byte

expressions:定义一到多个字节类型(1字节)的表达式。

(9)

.word

expressions:定义一到多个字类型(2字节)的表达式。

(10)

.long

expressions:定义一到多个长整数(4字节)类型的表达式。

(11)

.quad

bignums:定义一到多个8字节的长整数。

(12)

.fill

repeat,

size,

value:将value值拷贝repeat次,其中每个value占用size字节。如“.fill1024,4,0”会产生一个全0的页。

(13)

.space

size,

fill和.skip

size,

fill:在目标文件的当前位置处留出size字节的空间,并在其中填入值fill,如未指定fill,则填入0。

(14)

.rept

count和.endr:将.rept和.endr之间的行重复count次。

(15)

.set

symbol,

expression:将符号symbol的值设为expression。

(16)

.typename,

@type:

将符号name的type属性设为type。

其中type可以是function

(符号name是一个函数名)或object(符号name是一个数据对象)。

(17)

.sizename,expression:将符号name所占空间设为expression。

(18)

.global

symbol或.globl

symbol:使符号symbol成为全局的,即使该符号对链接程序是可见的。

(19)

.sectionname[,"flags"[,@type[,flag_specific_arguments]]]:切换当前节,即将下面的代码或数据汇编到name节中。其中flags可以是a(节是可分配的)、w(节是可写的)、x(节是可执行的);type可以是@progbits(节中包含数据)、@nobits(节中不含数据,只是占位空间)、@note(节中包含注释信息,不是程序)。

(20)

.subsection

num:切换当前子节,即将下面的代码或数据放在由num指定的子节中,节保持不变。

(21)

.text

subsection:切换当前子节,即将下面的程序汇编到.text节的编号为subsection的子节中。如未提供subsection,其缺省值为0。

(22)

.datasubsection:切换当前子节,即将下面的数据汇编到.data节的编号为subsection的子节中。如未提供subsection,其缺省值为0。

(23)

.previous:将当前节换回到前一个节与子节,即将下面的指令或数据汇编到当前节之前使用的节与子节中,如:

.sectionA

.subsection1

.word0x1234

.subsection2

.word0x5678 //0x5678放在subsection2中

.previous

.word0x9abc //0x1234与0x9abc放在subsection1中(24)

.code16:将下面的程序汇编成16位代码(实模式或保护模式)。

(25)

.code32:将下面的程序汇编成32位代码。2.4.2AT&T指令语法

与Intel指令相比,AT&T格式的指令有如下特点:

(1)指令操作数的顺序是先源后目的,与Intel指令的先目的后源的顺序相反。

(2)寄存器操作数前加前缀%,立即数前加前缀$。

(3)操作码带后缀以指明操作数的长度。后缀有b(8位)、w(16位)、l(32位)、q(64位)。在新版本的GUN汇编中,可以不带后缀。如:

moww%bx,%ax //将bx寄存器的内容拷贝到ax寄存器中

movw$1,%ax //将ax寄存器的值设为常数1

movlX,%eax //将变量X的值传递到eax寄存器中

(4)符号常数直接引用,如:

value:.long0x12345678 //定义一个双字类型的符号常量value

movlvalue,%ebx //ebx的值是0x12345678

引用符号常量的地址时,要在符号常量前加$,如:

movl$value,%ebx //将符号value的地址装入ebx

(5)大部分指令的操作码都与Intel指令相同,但也有几个例外,如:

lcall$S,$O //长调用,Intel的表示是callfarS:O

ljmp$S,$O //长跳转,Intel的表示是jmpfarS:O

lret$V //长返回,Intel的表示是retfarV

(6)内存间接寻址的写法是disp(base,index,scale),其意思是地址[base+disp+index*scale],如:

movl4(%ebp),%eax //从地址[ebp+4]中取1个长字给eax

movlary(,%eax,4),%eax //从地址[4*eax+ary]中取1个长字给eax

movwary(%ebx,%eax,4),%cx //从地址[ebx+4*eax+ary]中取1个字给cx

(7)

call和jmp的操作数前可以加“*”,以表示绝对地址,未加“*”表示相对地址(相对于EIP)。如call*%edi。

(8)允许使用局部标号(数字标号),而且允许重复定义局部标号。在以局部标号为目的的转移指令上,标号要带上后缀,b表示向后(已执行过的部分),f表示向前(未执行过的部分)。如:

1: jmp1f //跳到第3行

2: jmp1b //跳到第1行

1: jmp2f //跳到第4行

2: jmp1b //跳到第3行2.4.3GNU内嵌汇编

GCC允许在C语言代码中嵌入汇编代码,以实现C语言语法无法实现或不便实现的基础操作,如读写系统寄存器等。内嵌汇编的格式也是AT&T的,如下:

_asm__volatile_(

asmstatements

:outputs

:inputs

:registers-modified

);各部分的意义如下:

(1)

_asm_是一个宏,用于声明一个内嵌的汇编表达式,是必不可少的关键字。

(2)

_volatile_是一个宏,用于声明“不要优化该段内嵌汇编,让它们保持原样”。_volatile_是可选的。

(3)

asmstatements部分是一组AT&T格式的汇编语句,可以为空。一般情况下,在一行上应只写一个汇编语句。如果需要在一行上写多个语句,它们之间要用分号或“\n”(换行)隔开。所有的语句都要括在双引号内,可以用一对双引号,也可以用多对双引号。寄存器前面要加两个%做前缀。支持局部标号,且可以使用数字标号。

(4)

inputs部分指明内嵌汇编程序的输入参数。每个输入参数都括在一对圆括号内,各参数间用逗号分开。每个输入参数前都要加一到多个用双引号括起来的约束标志,用于向编译器声明该参数的输入位置(寄存器)及其相关信息。

(5)

outputs部分指明内嵌汇编程序的输出参数。每个输出变量都括在一对圆括号内,各个输出参数间用逗号隔开。每个输出参数前都要加一到多个用双引号括起来的约束标志,以告诉编译器从何处输出该参数及其相关信息。输出约束标志与输入约束标志相同,但前面还要多加一个“=”。输出参数应该是左值,而且必须是可写的。如果一个参数既做输出又做输入,可以在其前面加入“+”约束,也可以将它分成两个,一个写在outputs部分为只写参数,一个写在inputs部分为只读参数。

(6)输入和输出参数从0开始统一编号。一个编号可以唯一标识一个参数。在asmstatements部分可以通过标识号(加前缀%)来引用参数。在inputs部分,可以用标识号做输入约束标志(括在双引号内),告诉编译器将该输入参数与标识号所标识的输出参数放在同一个寄存器中。

(7)

registers-modified告诉编译器内嵌汇编程序将要修改的寄存器。每个寄存器都用双引号括起来,各寄存器间用逗号隔开。如果内嵌汇编程序中引用了某个特定的硬件寄存器,就应该在此处列出该寄存器,以告诉编译器这些寄存器的值被改变了。如果汇编程序中用某种不可预测的方式修改了内存,应该在此处加上“memory”。

内嵌汇编中常用的约束标志有下列几种:

“g”:让编译器决定将参数装入哪个寄存器。

“a”:将参数装入到ax/eax,或从ax/eax输出。

“b”:将参数装入到bx/ebx,或从bx/ebx输出。

“c”:将参数装入到cx/ecx,或从cx/ecx输出。“d”:将参数装入到dx/edx,或从dx/edx输出。

“D”:将参数装入到di/edi,或从di/edi输出。

“S”:将参数装入到si/esi,或从si/esi输出。

“q”:可以将参数装入到ax/eax、bx/ebx、cx/ecx或dx/edx寄存器中。

“r”:可以将参数装入到任一通用寄存器中。

“i”:整型立即数。

“m”:内存参数。

“p”:有效内存地址。

“=”:输出,参数是只写的左值。

“+”:既是输入参数又是输出参数。“&”:一般情况下,GCC将把输出参数和一个不相干的输入参数分配在同一个寄存器中,因为它假设在输出产生之前,所有的输入都已被消耗掉了。但如果内嵌的汇编程序有多条指令,这种假设就不再正确。在输出参数之前加入“&”,可以保证输出参数不会覆盖掉输入参数。此时,GCC将为该输出参数分配一个输入参数还没有使用到的寄存器,除非特殊声明(如用数字0~9)。

“0~9”:称为匹配约束标志,用于约束一个既做输入又做输出的参数,表示输入参数和输出参数占据同一个寄存器。数字约束标志只能出现在输入参数中,是与其共用同一位置的输出参数的编号。

inputs、outputs和registers-modified部分都可有可无。如有,顺序不能变;如无,应保留“:”,除非不引起二义性。

例:

#defineload_gdt(dtr)_asm__volatile("lgdt%0"::"m"(*dtr))

#defineswitch_to(prev,next,last)do{ \

unsignedlongesi,edi; \

asmvolatile( "pushl%%ebp\n\t" \

"movl%%esp,%0\n\t" /*saveESP*/ \

"movl%5,%%esp\n\t" /*restoreESP*/

\

"movl$1f,%1\n\t" /*saveEIP*/ \

"pushl%6

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论