深入介绍Linux内核
第三章-上篇
语言编译过程就是把人类能理解的高阶语言转換成电脑硬体能理解和执行的二进位机器指令的过程。这种转換过程通常会產生一些效率不是很高的代码,所以对一些执行效率要求高或性能影响较大的部分代码通常就会直接使用低级组合语言来编写,或者对高阶语言编译產生的组合语言程式再进行人工修改最佳化处理。本章主要描述Linux 0.12內核中使用的程式语言、目标档格式和编译环境,主要目标是提供閱读Linux 0.12內核原始码所需要的组合语言和GNU C语言扩展知识。首先比较详细地介绍了as86和GNU as组合语言程式的语法和使用方法,然后对GNU C语言中的行內组合(inline assemble)、语句运算式、暂存器变数以及行內函数等內核原始码中常用的C语言扩充內容进行了介绍,同时详细描述了C和组合函数之间的相互呼叫机制。因为理解目标档格式是了解组译器如何工作的重要前提之一,所以在介绍两种组合语言时会首先简单介绍一下目标档的基本格式,並在本章梢后部分再比较详细地给出Linux 0.12系统中使用的a.out目标档格式。最后简单描述了Makefile档的使用方法。
本章內容是閱读Linux內核原始码时的参考资讯。因此可以先大致流览一下本章內容,然后就閱读随后章节,在遇到问题时再回过头来参考本章內容。
3.1 as86组译器
在Linux 0.1x系统中使用了两种组译器(Assembler)。一种是能產生16位元代码的as86组译器,使用配套的ld86链结器;另一种是GNU的组译器gas (as),使用GNU 1d链结器来链结產生的目标档。这裡我们首先說明as86组译器的使用方法,as组译器的使用方法放在下一节中进行說明。
as86和ld86是由MINIX一386的主要开发者之一Bruce Evans编写的Intel 8086、80386组合编译程式和链结程式。在刚开始开发Linux內核时Linus就已经把它移植到了Linux系统上。它虽然可以为80386处理器编制32位元代码,但是Linux系统仅用它来建立16位元的啟动开机磁区程式boot/bootsect.s和真实模式下初始设置程式boot/setup.s 的二进位执行代码。该编译器快速小巧,并具有一些GNU gas所沒有的特性,例如巨集以及更多的错误检测手法。不过该编译器的语法与GNU as组合编译器的语法不相容而更近似於微软的MASM、Borland公司的Turbo ASM和NASM等组译器的语法。这些组译器都使用了Intel的组合语言语法(如运算元的次序与GNU as的相反等)。
as86的语法是基於MINIX系统的组合语言语法,而MINIX系统的组合语法则是基於PC/IX系统的组译器语法。 PC/IX是很早以前Intel 8086 CPU上执行的一个UN*X类作业系统,Andrew S.Tanenbaum就是在PC/IX系统上进行MINIX系统开发工作的。
Bruce Evans是MINIX作业系统32位元版本的主要修改编制者之一,他与Linux的创始人Linus Torvalds是好友。在Linux內核开发初期,Linus从Bruce Evans那裡学到了不少有关UNIX类作业系统的知识。MINIX作业系统的不足之处也是两个好朋友互相探讨得出的结果。MINIX的这些缺点正是激发Linus在Intel 80386体系结构上开发一个全新概念作业系统的主要动力之一。Linus曾经說过:“Bruce是我的英雄”,因此我们可以說Linux作业系统的诞生与Bruce Evans也有著密切的关系。
有关这个编译器和连接器的原始码可以从FTP伺服器ftp.funet.fi上或从网站下载。现代Linux系统上可以直接安装包含as86/ld86的RPM套装软体,例如dev86-0.16.30-8.i386.rpm。由於Linux系统仅使用as86和ld86编译和链结上面提到的两个16位组合语言程式bootsect.s和setup.s,因此这裡仅介绍这两个程式中用到的一些组合语言程式语法和组合命令(组合指示符号)的作用和用途。
3.1.1 as86组合语言语法
组译器专门用来把低级组合语言程式编译成含机器码的二进位程式或目标档。组译器会把输入的一个组合语言程式(例如srcfile)编译成目标档(objfile)。组合的命令行基本格式是:
as [选项] -o objfile srcfile
其中选项用来控制编译过程以產生指定格式和设置的目标档。输入的组合语言程式srcfile是一个文字档案。该档案內容必须是由換行字元结尾的一系列文字行组成。虽然GNU as 可使用分号在一行上包含多个语句,但通常在编制组合语言程式时每行只包含一条语句。
语句可以是只包含空格、跳位字元和換行符号的空行,也可以是代入语句(或定义语句)、虛拟操作符号语句和机器指令语句。代入语句用於给一个符号或识别字代入。它由识別字后跟一个等号,再跟一个运算式组成,例如:“BOOTSEG = 0VX 07C0” 。虛拟操作符号(pseudo operation)语句是组译器使用的指示符号,它通常並不会產生任何代码它由虛拟操作码和0个或多个操作阵列组成,每个操作码都由一个点字元‘.’开始。点字元‘.’本身是一个特殊的符号,它表示编译过程中的位置计数器。其值是点符号出现处机器指令第l个位元组的位址。
机器指令语句是可执行机器指令的助忆符号,它由操作码和0个或多个运算元构成。另外,任何语句之前都可以有标号。标号是由一个识別字后跟一个冒号‘:’ 组成。在编译过程中,当组译器遇到一个标号,那麼当前位置计数器的值就会代入给这个标号。因此一条组合语句通常由标号(可选) 、指令助忆符号(指令名)和运算元三个栏位组成,标号位於一条指令的第一个栏位。它代表其所在位置的位址,通常指明一个跳转指令的目标位置。最后还可以跟随用注释符开始的注释部分。
组译器编译產生的目标档 objfile 通常起码包含三个段或区¹ (section) ,即正文段(.text) 、资料段(.data) 和未初始化资料段(.bss) 。正文段(或称为代码段)是一个已初始化过的段,通常其中包含程式的执行代码和唯读资料。资料段也是一个已初始化过的段,其中包含有可读/写的资料。而未初始化资料段是一个未初始化的段。通常组译器產生的输出目标档中不会为该段保留空间,但在目标档链结成执行程式被载入时作业系统会把该段的內容全部初始化为0。在编译过程中,组合语言程式中会產生代码或资料的语句,都会在这三个中的一个段中生成代码或资料。 编译產生的位元组会从‘.text’ 段开始存放。我们可以使用段控制虛拟操作符号来更改写入的段。目标档格式将在后面“Linux 0.12目标档格式”一节中加以详细說明。
————————————————————————————————————————————————————— 有关目标档中术语“section“对应的中文名称有多种。在UNIX作业系统早期阶段,该术语在目标档中均称为”segment”。这是因为早期目标档中的段可以直接对应到电脑处理器中段的概念上,但是由於现在目标档中的段的概念已经与处理器中的段暂存器沒有直接对应关系,並且容易把这两者混淆起来,因此现在英文文献中均使用“section“取代目标档中的“segment”命名。这也可以从GNU使用手冊的各个版本变迁中观察到。section的中文译法有“段、区、节、部分和区域”等几种。我们在不至於混淆处理器段概念前提下会根据所述內容把“section”称为“段”、“区”或“部分”,但主要採用“区”这个名称.
3.1.2 as86组合语言程式
下面我们以一个简单的框架示例程式boot.s来說明as86组合语言程式的结构以及程式中语句的语法,然后给出编译链结和执行方法,最后再分別列出as86和ld86的使用方法和编制选项。示例程式见如下所示。这个示例是bootsect.s的一个框架程式,能编译生成开机磁区代码。其中为了演示說明某些语句的使用方法,故意加入了无意的第20行语句。
1 ! 2 !boot.s- - bootsect.s的框架程式用代码0x07替換串msgl中1字元,然后在萤幕第1行上显示。 3 ! 4 globl begtext,begdata,begbss,endtext,enddata,endbss !全域识别字,供1d86 链结使用; 5 text !正文段 6 begtext : 7 .data !资料段 8 begdata : 9 .bss !末初始化资料段 10 begbss : 11 .text !正文段 12 13 BOOTSEG = 0x07c0 !BIOS载入bootsect代码的原始段地址; 14 15 entry start !告知链结程式,程式从start标号处开始执行。 16 start : 17 jmpi go,BOOTSEG !段间跳转。INITSEG指出跳转段位址,标号go是偏移位址。 18 go : mov ax,cs !段暂存器cs值- -〉ax。 19 mov ds,ax 20 mov [msgl+17],ah !0x07--〉替换字串中1个点符号,喇叭将会鸣一声。 21 mov cx,#20 ! 共显示20个字元,包括归位换行符号。 22 mov dx,#0x1004 !字串将显示在荧幕第17行、第5列处。 23 mov bx,#0x000c !字元显示属性(红色)。 24 mov bp,#msg1 !指向要显示的字串(中断呼叫要求)。 25 mov ax,#0x1301 !写字串并移动游标到串结尾处。 26 int 0x10 !BIOS中断呼叫0x10,功能0x13,子功能01。 27 loop : jmp loop !闭环。 28 msg1 :.ascii ”Loading system ...“ !呼叫BIOS中断显示的资讯。共20个ASCII码字元。 29 .byte 13,10 30 .org 510 !表示以后语句从位510(0x1FE)开始存放。 31 .word 0xAA55 !用效开机磁区旗标,供BIOS载入开机磁区使用。 32 .text 33 endtext : 34 .data 35 enddata : 36 .bbs 37 endbss :
我们首先介绍该程式的功能,然后再详细說明各语句的作用。该程式是一个简单的开机磁区啟动程式。编译链结產生的执行程式可以放入软碟第l个磁区直接用来引导电脑啟动。啟动后会在萤幕第17行、第5列处显示出红色字串主“Loading system...” ,並且游标下移一行。然后程式就在第26行上闭环。
该程式开始的3行是注释语句。在as86组合语言程式中,凡是以感叹号‘!’或分号‘;’开始的语句其后面均为注释文字。注释语句可以放在任何语句的后面,也可以从一个新行开始。
第4行上的‘.globl’是组合指示符号(或称为组合虛拟指令、虛拟操作符号)。组合指示符号均以一个字元‘.’开始,並且不会在编译时產生任何代码。组合指示符号由一个虛拟操作码,后跟。个或多个操作阵列成。例如第4行上的‘globl’是一个虛拟操作码,而其后面的标号‘begtext,begdata,begbss’等标号就是它的运算元。标号是后面带冒号的识別字,例如第6行上的 ‘begtext:’但是在引用一个标号时无须带冒号。
通常一个组译器都支援很多不同的虛拟操作符号,但是下面仅說明Linux系统bootsect.s和setup.s组合语言程式用到的和一些常用的as86虛拟操作符号。
‘.globl’虛拟操作符号用於定义随后的标号识別字是外部的或全域的,並且即使不使用也強制引入。
第5行到第11行上除定义了3个标号外,还定义了3个虛拟操作符号:‘.text’、‘.data’、‘.bbs’。它们分別对应组合语言程式编译產生目标档中的3个段,即正文段、资料段和未初始化资料段。‘.text’用於标识正文段的开始位置,並把切換到text段;‘.data’用於标识资料段的开始位置,並把当前段切換到data段;而‘.bbs’则用於标识一个未初始化资料段的开始,並把当前段改变成bbs段。因此行5-11用於在每个段中定义一个标号,最后再切換到text段开始编写随后的代码。这裡把三个段都定义在同一重疊位址范围中,因此本示例程式实际上不分段。
第13行定义了一个代入语句"B007SEG = 0x07c0"。等号 ‘=’(或符号‘EQU’)用於定义识別字B00TSEG所代表的值,因此这个识別字可称为符号常数。这个值与C语言中的写法一樣,可以使用十进位,八进位和十六进位。
第15行上的识別字 ‘entry’是保留关键字,用於迫使链结器ld86在生成的可执行档中包括进其后指定的标号‘start’。通常在链结多个目标档生成一个可执行档时应该在其中一个组合语言程式中用关键字entry指定一个入口标号,以便於除错。但是在我们这个示例中以及Linux內核boot/bootsect.s和boot/setup.s组合语言程式中完全可以省略这个关键字,因为我们並不希望在生成的纯二进位执行档中包括任何符号资讯。 .
第17行上是一个段问(Inter-segment)远跳转语句,就跳转到下一条指令。由於当BIOS把程式载入到实体记忆体0x7c00处並跳转到该处时,所有段暂存器(包括CS)预设值均为0,即此时CS:IP = 0x0000:0x7c00。因此这裡使用段问跳转语句就是为了给CS代入段值0x7c0。该语句执行后CS:IP:0x07C0:0x0005。随后的两条语句分別给DS和ES段暂存器代入,让它们都指向0x7c0段。这樣便於对程式中的资料(字串)进行定址。
第20行上的MOV指令用於把ah暂存器中0x7c0段值的高位元组(0x07)存放到记忆体中字串msgl最后一个´.´ 位置处。这个字元将导致BIOS中断在显示字串时鸣叫一声。使用这条语句主要是为了说明间接运算元的用法。在as86中,间接运算元需要使用方括号对。另外一写定址方式有以下一些:
! 接暂存器定址。跳转到bX值指定的地址处,即把bx的值复制到IP中。 mov bx,ax jmp bx
! 间接暂存器定址。bx值指定记亿体位置处的內容作为跳转的位址。 mov [bx],ax jmp [bx],
! 把立即数1234放到ax中。把msgl地址值放到ax中。 mov ax,#1234 mov ax,#msgl
! 絕对定位。把记忆体位址1234 (msgl) 处的內容放人ax中。 mov ax,1234 mov ax,msgl mov ax,[msgl]
! 索引定址。把第2个运算元所指记忆体位置出的值放入ax中。 mov ax,msgl [bx] mov ax,mgsl [bx*4+si]
第2l-25行的语句分別用於把立即数放到相应的暂存器中。立即数前一定要加井号‘#’,否则将作为记忆体位址使用而使语句变成絕对定位语句,见上面示例另外。把一个标号(例如msgl)的位址值放入暂存器中时也一定要在前面加‘#’,否则会变成把msgl位址处的內容放到了暂存器中!
第26行是BIOS萤幕显示中断呼叫int 0x10 这裡使用其功能16、子功能1。 该中断的作用是把一字串(msg1)写到萤幕指定位置处。暂存器cx中是字串长度值,dx中是显示位置值,bx中是显示使用的字元属性,es : bp指向字串。
第27行是一个跳转语句,跳转到当前指令处。因此这是一个闭环语句。这里採用闭环语句是为了让显示的內容能夠停留在萤幕上而不被刪除。闭环语句是除错组合语言程式时常用的方法。
第28-29行定义了字串msgl。定义字串需要使用虛拟操作符号‘.ascii’,並且需要使用双引号括住字串。虛拟操作符号‘.asciiz’ 还会自动在字串后添加一个NULL (0)字元。另外,第29行上定义了两个归位換行(13,10)字元。定义字元需要使用虛拟操作符号‘.byte’,並且需要使用单引号把字元括住。例如:“‘D’”当然我们也可以象示例中的一樣直接写出字元的ASCII码。
第30行上的虛拟操作符号语句‘.org’定义了当前组合的位置。这条语句会把组译器编译过程中当前段的位置计数器值调整为该虛拟操作符号语句上给出的值。对於本示例程式,该语句把位置计数器设置为510,並在此处(第31行)放置了有效开机磁区旗标字0xAA55。虛拟操作符号‘.word’用於在当前位置定义一个双位元组记忆体物件(变数) ,其后可以足一个数或者是一个运算式。由於后面沒有代码或资料了,因此我们可以据此确定boot.s编译出来的执行程式应该正好为512位元组。
第32-37行又在三个段中分別放置了三个标号。分別用来表示三个段的结束位置。这樣设置可以用来在链结多个目的模组时区分各个模组中各段的开始和结束位置。由於內核中的bootsec.s和setup.s程式都是单独编译链结的程式,各自期望生成的都是纯二进位档而並沒有与其他目的模组档进行链结,因此示例程式中宣告各个段的虛拟操作符号(.text、.data和.bss)都完全可以省略掉,即程式中第4—12行和32—37行可以全部刪除也能编译链结產生出正确的结果。
3.1.3 as86组合语言程式的编译和链结
现在我们說明如何编译链结示例程式boot.s来生成我们需要开机磁区程式boot。编译和链结上面示例程式需要执行以下前两条命令:
[/root]# as86 -0 -a -o boot.o boot.s //编译。生成与as部分相容的目标档。 [/root]# 1d86 -0 -a -o boot.o //链结。去掉符号资讯。 [/root]# 11 boot* -rwx--x--x 1 root root 544 May 17 00:44 boot -rw------- 1 root root 249 May 17 0043 boot.o -rw------- 1 root root 767 May 16 23:27 boot.s [/root]# dd bs=32 if = boot of=/dev/fd0 skep=1 //写入软碟或Image碟档中。 16+0 records in 10+0 records out [/root]# _
其中第l条命令利用as86组译器对boot.s程式进行编译,生成boot.o目标档。第2条命令使用链结器1d86对目标档执行链结操作,最后生成MINIX结构的可执行档boot。其中选项‘-0’用於生成8086的16位目的程式:‘-a’用於指定当成与GNU as和ld部分相容的代码。‘-s’选项用於告诉链结器要去除最后生成的可执行档中的符号资讯。‘-o’指定生成的可执行档案名称。
从上面1l命令列出的档案名中可以看出,最后生成的boot程式並不是前面所說的正好512位元组,而是长了32位元组。这32位元组就是MINIX可执行档的标头结构(其详细结构說明请参见“內核组建工具”一章內容) 。为了能使用这个程式开机啟动机器,我们需要人工去掉这32位元组。去掉该标头结构的方法有几种:
▓ 使用二进位编辑程式刪除boot程式前32位元组,並存档;
▓ 使用现在Linux系统(例如RedHat 9)上的as86编译链结程式,它们具有可生成不带MINIX标头结构的纯二进位执行档的选项,请参考相关系统的線上使用手冊页(man as86)。
▓ 利用Linux系统的dd命令。
上面列出的第3条命令就是利用dd命令来去除boot中的前32位元组,並把输出结果直接写到软碟或Bochs模拟系统的软碟映射档中(有关Bochs Pc模拟系统的使用方法请参考最后一章內容)。若在Bochs模拟系统中执行该程式,我们可得到如图3-1所示画面。
3.1.4 as86和1d86使用方法和选项
as86和ld86的使用方法和选项如下: s的使用方法和选项: as [-03agjuw] [-b [bin]] [-lm [1ist]] [-n name] [-o objfile] [-s syn] srcfile
预设设置(除了以下预设值以外,其他选项预设为关闭或无;若沒有明确說明a旗标,则不会有输出) :
-3 使用80386的32位输出; list 在标準输出上显示: name 原始档案的基本名称 (即不包括‘.’后的副档名) ;
各选项含义:
-0 使用16bit代码段; -3 使用32bit代码段; -a 开啟与GNU as、ld的部分相容性选项; -b 產生二进位档,后面可以跟档案名称; -g 在目标档中仅存入全域符号; -j 使所有跳转语句均为长跳转; -l 產生列表档,后面可以跟随列表档案名; -m 在列表中扩充巨集定义; -n 后面跟随模组名称(取代原始档案名称放入目标档中) ; -o 產生目标档,后跟目标档案名(objfile) ; -s 產生符号档,后跟符号档案名; -u 将未定义符号作为输入的未指定段的符号; -w 不显示警告资讯:
ld连接器的使用语法和选项: 对於生成Minix a.out格式的版本: ld [-03Mims [-]] [-T textaddr] [-llib_extension] [-o outfile] infile...
对於生成GNU—Minix的a.out格式的版本: ld [-03Mims [-]] [-T textaddr] [-llib_extension] [-o outfile] infile...
预设设置(除了以下预设值以外,其他选项预设为关闭或无) :
-03 32位输出; outfile a.out格式输出: -0 產生具有16bit魔数的标头结构,並且对 -lx选项使用i86子目錄; -3 產生具有32bit魔数的标头结构,並且对 -lx选项使用i386子目錄; -M 在标準输出设备上显示已链结的符号; -T 后面跟随正文基底位址(使用适合於strtoul的格式) ; -i 分离的指令与资料段(I&D)输出; -lx 将程式库 /local/lib/subdir/libx.a加入链结的档列表中; -m 在标準输出设备上显示已链结的模组; -o 指定输出档案名,后跟输出档案名; -r 產生适合於进一步重定位的输出; -s 在目标档中刪除所有符号。
3.2 GNU as组译
上节介绍的as86组译器仅用於编译內核中的boot/bootsect.s开机磁区程式和真实模式下的设置程式boot/setup.s。內核中其余所有组合语言程式(包括C语言產生的组合语言程式)均使用gas来编译,並与C语言程式编译產生的模组链结,本节以80X86 CPU硬体平台为基础介绍Linux內核中使用组合语言程式语法和GNU as组译器(简称as组译器)的使用方法。我们首先介绍as组合语言程式的语法,然后给出常用组合虛拟指令(指示符号)的含义和使用方法。带有详细說明资讯的as组合语言程式实例将在下一章最后给出。
由於作业系统许多关键代码要求有很高的执行速度和效率,因此在一个作业系统原始码中通常就会包含大約10%左右的起关键作用的组合语言程式量。Linux作业系统也不例外,它的32位元初始化代码、所有中断和異常处理过程介面程式、以及很多巨集定义都使用了as组合语言程式或扩充的嵌入组合语句。是否能夠理解这些组合语言程式的功能也就无疑成为理解一个作业系统具体实现的关键点之一。
在编译C语言程式时,GNU gcc编译器会首先输出一个作为中间结果的as组合语言档,然后gcc会呼叫as组译器把这个临时组合语言程式编译成目标档。 即实际上as组译器最初是专门用於组合gcc產生的中间组合语言程式的,而非作为一个独立的组译器使用。因此,as组译器也支援很多C语言特性,这包括字元,数字和常数表示方法以及运算式形式等方面。
GNU as组译器最初是仿照BSD 4.2的组译器进行开发的。现在的as组译器能夠配置成產生很多不同格式的目标档。虽然编制的as组合语言程式与具体採用或生成什麼格式的目标档关系不大,但是在下面介绍中若有涉及目标档格式时,我们将围绕Linux 0.12系统採用的a.out目标档格式进行說明。
3.2.1 编译as组合语言程式
使用as组译器编译一个as组合语言程式的基本命令行格式如下所示:
as [选项] [ -o objfile] [srcfile.s ...]
其中objfile是as 编译输出的目标档案名,srcfile.s是as的输入组合语言程式名。如果沒有使用输出档案名,那麼as会编译输出名称为a.out的预设目标档。產生在as程式名之后,命令行上可包含编译选项和档案名,所有选项可随意放置,但是档案名的放置次序编译结果密切相关。
一个程式的来源程式可以被放置在一个或多个档中,程式的原始码是如何分割放置在几个档案中並不会改变程式的语义。程式的原始码是所有这些档案依序的组合结果。每次执行as编译器,它只编译一个来源程式。但一个来源程式可由多个文字档案组成(终端的标準输入也是一个档) 。
我们可以在as命令行上给出零个或多个输入档案名称,as将会按从左到右的顺序读取这些输入档的內容。在命令行上任何位置处的参数若沒有特定含义的话,将会被作为一个输入档案名看待。如果在命令行上给出任何档案名,那麼as将会试图从终端或主控台标準输入中读取输入档內容。在这种情況下,若已沒有內容要输入时就需要手工键入Ctrl-D组合键来告知as组译。若想在命令行上明确指明把标準输入作为输入档,那麼就需要使用参数‘┅’。
as的输出档是输入的组合语言程式编译生成的二进位资料档,即目标档。除非我们使用选项‘-o’指定输出档的名称,否则as将產生名为a.out的输出档。目标档主要用於作为链结器1d的输入档。目标档中包含有已组合过的程式码、协助ld產生可执行程式的资讯、以及可能还包含除错符号资讯。Linux 0.12系统中使用的a.out目标档格式将在本章后面进行說明。
假如我们想单独编译boot/head.s组合语言程式,那麼可以在命令行上键入如下形式的命令:
[/usr/src/linux/boot]# as -o head.o head.s [/usr/src/linux/boot]# ls -l head* rw-rwxr-x l root root 26449 May 19 22:04 head.o rw-rwxr-x 1 root root 5938 Nov 18 1991 head.s [/usr/src/linuxboot]#
3.2.2 as组合语法
为了维持与gcc输出组合语言程式的相容性,as组译器使用AT & T系统V的组合语法 (下面简称为AT & T语法) 。这种语法与Intel 组合语言程式使用的语法(简称Intel语法)很不一樣,它们之问的主要区別有一些几点:
▓ AT & T语法中立即运算元前面要加一个字元‘$’;暂存器运算元名前要加字元百分号‘%’;絕对跳转/呼叫(相对於与程式计数器有关的跳转/呼叫)运算元前面要加星号‘*’。而Intel组合语法均沒有这些限制。
▓ AT & T语法与Intel语法使用的来源和目的运算元次序正好相反。AT & T的源和目的运算元是从左到右‘源,目的’。例如Intel的语句‘add eax,4’对应AT & T的‘addl $4, %eax’。
▓ AT & T语法中记忆体运算元的长度(宽度)由操作码最后一个字元来确定。操作码副档名‘b’、‘w’和‘1’分別指示记忆体引用宽度为8位元位元组(byte)、16位元 字(word)和32位长字(long)。Intel语法则透过在记忆体运算元前使用首码‘byte prt’、‘word ptr’和‘dword ptr’来达到同樣目的。因此,Intel的语句‘mov al,byte ptr foo’对应于AT & T的语句‘mov $foo,%al’。
▓ AT & T语法中立即形式的远眺转和远呼叫为‘ljmp/call $section,$offset’,而Intel的是‘jmp/call far section:offset’。同樣,AT & T语法中远返回指令‘lret $stack-adjust’对应Intel的‘ret far stack-adjust’。
▓ AT & T组译器不提供对多代码段程式的支援,UNIX类作业系统要求所有代码在一个段中。
组合语言程式预处理
as组译器具有对组合语言程式內置的简单预处理(preprocess)功能。该预处理功能会调整並刪除多余的空格字元和跳位字元;刪除所有注释语句並且使用单个空格或一些換行符号替換它们;把字元常数转換成对应的数值。但是该预处理功能不会对巨集定义进行处理,也沒有处理引入档 (include file) 的功能。如果需要这方面的功能,那麼可以让组合语言程式使用大写的副档名‘S’让as使用gcc的CPP预处理功能o
由於as组合语言程式除了使用C语言注释语句(即‘/*’和‘*/’)以外,还使用井号‘#’作为单行注释开始字元,因此若在组合之前不对程式执行预处理,那麼程式中包含的所有以井号‘#’开始的指示符号或命令均会被当作注释部分。
符号、语句和常数
符号(Symbol)是由字元组成的识別字,组成符号的有效字元取自於大小写字元集、数位和三个字元‘_.$’。符号不允许用数位字元开始,並且大小写含义不同。 在as组合语言程式中符号长度沒有限制,並且符号中所有字元都足有效的。符号使用其他字元(例如空格、換行符号) 或者档的开始来界定开始和结束处。
语句(Statement)以換行符号或者行分割字元(‘;’)作为结束。档最后语句必须以換行符号作为结束。
若在一行的最后使用反斜線字元‘’(在換行符号前) ,那麼就可以让一条语句使用多行。当as读取到反斜線加換行符号时,就会怱略掉这两个字元。 - 语句由零个或多个标号(Label)开始,后面可以跟随一个确定语句类型的关键符号。标号由符号后面跟随一个冒号(‘:’)构成。关键符号确定了语句余下部分的语义。如果该关键符号以一个‘.’开始,那麼当前语句就是一个组合命令(或称为虛拟指令、指示符号)。如果关键符号以一个字母开始,那麼当前语句就是一条组合语言指令语句。因此一条语句的通用格式为:
标号 : 组合命令 注释部分 (可选) 或 标号 : 指令助忆符号运算元1,运算元2 注释部分 (可选)
常数是一个数值,可分为字元常数和数值常数两类。字元常数还可分为字串和单个字元;而数值常数可分为整数、大数和浮点数。
字串必须用双引号括住,並且其中可以使用反斜線‘’来转义包含特殊字元。例如‘’表示一个反斜線字元。其中第1个反斜線是转义指示字元,說明把第2个字元看作一个普通反斜線字元。常用转义符序列见表3-1所示。反斜線后若是其他字元,那麼该反斜線将不起作用並且as 组译器将会发出警告资讯。
组合语言程式中使用单个字元常数时可以写成在该字元前加一个单引号,例如“‘A”表示值65、“‘C”表示值67。表3-1中的转义码也同樣可以用於单个字元常数。
整数数值常数有4种表示方法,即使用‘ob’或‘OB’开始的二进位数值(‘0-l’);以‘0’开始的八进位数(‘0-7’);以非‘0’数字开始的十进位数字(‘0-9’)和使用‘0x’或‘0X’开头的十六进位数(‘0-9a-fA-F’) 。若要表示负数,只需在前面添加负号‘-’。
大数(Bignum)是位元数超过32位元二进位位元的数值,其表示方法与整数的相同。组合语言程式中对浮点常数的表示方法与C语言中的基本一樣。由於內核代码中几乎不用浮点数,因此这裡不再对其进行說明。
3.2.3 指令语句、运算元和定址
指令(Instructions)是CPU执行的操作,通常指令也称作操作码(Opcode):运算元(Operand)是指令操作的对像:而位址(Address)是指定资料在记忆体中的位置。指令语句是程式执行时刻执行的一条语句,它通常可包含4个组成部分:
▓ 标号(可选) :
▓ 操作码(指令助忆符号):
▓ 运算元(由具体指令指定):
▓ 注释
一条指令语句可以含有0个或最多3个用逗号分开的运算元。对於具有两个元的指令语句,第l个是源运算元,第2个是目的运算元,即指令操作结果保存在第2个运算元中。
运算元可以是立即数(即值是常数值的运算式) 、暂存器(值在CPU的暂存器中)或记忆体(值在记忆体中) 。 1个间接运算元(Indirect operand)含有实际运算元值的地址值。AT & T语法透过在运算元前加一个‘*’字元来指定一个间接运算元。只有调转/呼叫指令才能使用间接运算元。
▓ 立即运算元前需要加一个‘$’字元首码;
▓ 暂存器名前需要加一个‘%’字元首码;
▓ 记忆体运算元由变数名或者含有变数位址的一个暂存器指定。变数名隐含指出了变数的位址並指示CPU参照引用该位址处记忆体的內容。
指令操作码的命名
AT & T语法中指令操作码名称(即指令助忆符号)最后一个字元用来指明运算元的宽度。字元‘b’、‘w’和分別指定byte 、word和long类型的运算元。如果指令名称沒有带这樣的字元副档名,並且指令语句中不合记忆体运算元,那麼as就会根据目的暂存器运算元来尝试确定运算元宽度。例如指令语句‘mov %ax,%bx’等同於‘movw %ax,%bx’。同樣,语句‘mov $1,%bx’等同於‘movw $l,%bx’。
AT & T与Intel语法中几乎所有指令操作码的名称都相同,但仍有几个例外。符号扩展和零扩展指令都需要2个宽度来指明,即需要为源和目的运算元指明宽度.AT & T语法中是透过使用两个操作码副档名来做到。AT & T语法中符号扩充和零扩充的基本操作码名称分別是‘movs…’和‘movz…’,Intel中分別是‘movsx’和‘movzx’。两个副档名就附在操作码基本名上。例如“使用符号扩展从%al移动到%edx”的AT & T语句是‘movsbl %al,%edx’,即从byte到long是bl从byte到word是bw、从word到long是wl。AT & T语法与Intel语法中转換指令的对应关系见表3-2所示。
指令操作码首码
操作码首码用於修饰随后的操作码。它们用於重复字串指令、提供区覆盖、执行汇流排锁定操作、或指定运算元和位址宽度。通常操作码首码可作为一条没有运算元的指令独佔一行並且必须直接位於所影响指令之前,但是最好与它修饰的指令放在同一行上。例如,扫描指令‘scas’使用首码执行重复操作:
repne scas %es :(%edi), %al
图表 3-3 中列出一些操作首码
记忆体引用参照
Intel语法的间接记忆体引用形式:section:[base +i ndex*scale + disp] 对应於如下AT & T语法形式:section:disp (base,index,scale)
其中base和index是可选的32位元基暂存器和索引暂存器,disp是可选的偏移值。scale是比例因数,取值范围是 1、2、4和8。scale其乘上索引index用来计算运算元位址。如果沒有指定scale,则scale取预设值l。section为记忆体运算元指定可选的段暂存器,並且会覆盖运算元使用的当前预设段暂存器。请注意,如果指定的段覆盖暂存器与预设操作的段暂存器相同,则as就不会为组合的指令再输出相同的段首码。以下是几个AT & T和Intel语法形式的记忆体引用例子:
movl var,%eax # 把记忆体位址var处的內容放入暂存器%eax中。 movl %cs : var,%eax # 把代码段中记忆体位址var处的內容放入%eax中。 movb $0x0a,%es : (%ebx) # 把位元组值0x0a保存到es段的%ebx指定的偏移处。 mov1 $var, %eax # 把var的地址放入%eax中。 mov1 array(%esi), eax # 把arry+%esi确定的记忆体位址处的内容放入%eax中。 mov1 (%ebx, %esi, 4), %eax # 把%ebx + %esi*4 确定的记忆体位址处的内容放入%eax中。 mov1 arry(%ebx,%esi, 4), %eax # 把arry + %ebx+esi*4 确定记忆体位址处的内容放入%eax中。 mov1 -4(%ebp), %eax # 把%ebp -4 记忆体位址处的内容放入%eax中,使用预设段%ess。 mov1 foo(,%eax, 4), %eax # 把记忆体位址foo + eax*4处内容放入%eax中,使用预设段%ds。
另外,与指令计数器PC²无关的间接呼叫和跳转的运算元必须有一个‘*’首码字元。若沒有使用‘*’字元,那麼as组译器就会选择与指令计数PC相关的跳转/呼叫标号。其他任何具有记忆体运算元的指令都必须使用操作码副档名(‘b’、‘w’或‘l’)指明运算元的大小(byte、word或long) 。
3.2.4 区与重定位
区(Section) (也称为段、节或部分)用於表示一个位址范围,作业系统将会以相同的方式对待和处理在该位址范围中的资料资讯。例如,可以有一个“唯读”的区,我们只能从该区中读取资料而不能写入。区的概念主要用来表示编译器生成的目标档(或可执行程式)中不同的资讯区域,例如目标档中的正文区或资料区。若要正确理解和编制一个as组合语言程式,我们就需要了解as產生的输出目标档的格式安排。有关Linux 0.12內核使用的a.out格式目标档格式的详细說明将在本章后面陈述,这裡首先对区的基本概念作一简单介绍,以理解as组译器產生的目标档基本结构。
链结器Id会把输入的目标档中的內容按照一定规律组合生成一个可执行程式。当as组译器输出一个目标档时,该目标档中的代码被预设设置成从位址0开始。此后ld将会在链结过程中为不同目标档中的各个部分分配不同的最终位址位置。 ld会把程式中的位元组块移动到程式执行时的位址处。这些块是作为固定单元进行移动的。它们的长度以及位元组次序都不会被改变。这樣的固定单元就被称作是区(或段、部分)。而为区分配执行时刻的位址的操作就被称为重定位(Relocation)操作,其中包括调整目标档中记錄的位址,从而让它们对应到恰当的执行时刻位址上。
as组译器输出產生的目标档中至少具有3个区,分別被称为正文(text) 、资料(data)和bss区。每个区都可能是空的。如果沒有使用组合命令把输出放置在‘.text’或‘.data’区中,这些区会仍然存在,但內容是空的。在一个目标档中,其text区从位址0开始,随后是data区,再后面足bss区。
当一区被重定位时,为了让链结器ld知道哪些资料会发生变化以及如何修理这些资料,,as组译器也会往目标档中写入所需要的重定位资讯。为了执行重定位操作,在每次涉及目标档中的一个位址时,1d必须知道:
▓ 目标档中对一个位址的引用是从什麼地方算起的?
▓ 该引用的位元组长度是多少?
▓ 该位址引用的是哪个区? (位址) – (区的开始位址) 的值等於多少?
▓ 对位址的引用与程式计数器PC (Program-Counter)相关吗?
实际上,as使用的所有位址都可表示为:(区) + (区中偏移)。另外,as计算的大多数运算式都有这种与区相关的特性。在下面說明中,我们使用记号“{secname N}”来表示区secname中偏移N。
除了text、data和bss区,我们还需要了解絕对位址区(absolute区)。 当链结器把各个目标档组合在一起时,absolute区中的位址将始终不变。例如,ld会把位址{absolute 0} “重定位”到执行时刻位址0处。尽管链结器在链结后决不会把两个目标档中的data区安排成重疊位址处,但是目标档中的absolute区必会重疊而覆盖。
另外还有一种名为“未定义的”区(Undefined section)。在组合时不能确定所在区的任何位址都被设置成{undefined U},其中U将会在以后填上。因为数值总是有定义的,所以出现未定义位址的唯一途径仅涉及未定义的符号。对一个称为公共区块(common block)的引用就是这樣一种符号:在组合时它的值未知,因此它在undefined区中。
类似地,区名也用於描述已链结程式中区的组。链结器1d会把程式所有目标档中的text区放在相邻的位址处。我们习惯上所說的程式的text区实际上是指其所有目标档text区组合构成的整个位址区域。对程式中data和bss区的理解也同樣如此。
键结器涉及的区
链结器ld只涉及如下4类区:
▓ text区、data区一这两个区用於保存程式。as和ld会分別独立而同等地对待它们。对其中text区的描述也同樣适合於data区。然而当程式在执行时,则通常text区是不会改变的。text区通常会被行程共用,其中含有指令代码和常数等內容。程式执行时data区的內容通常是会变化的,例如,C变数一般就存放在data区中。
▓ bss区- -在程式开始执行时这个区中含有0值位元组。该区用於存放未初始化的变数或作为公共变数储存空间。虽然程式每个目标档bss区的长度资讯很重要,但是由於该区中存放的是0值位元组,因此无须在目标档中保存bss区。设置bss区的目的就是为了从目标档中明确地排除0值位元组.
▓ absolute区一该区的位址0总是“重定位”到执行时刻位址0处。如果你不想让ld在重定位操作时改变你所引用的位址,那麼就使用这个区,从这种观点来看,我们可以把絕对位址称作足“不可重定位的”:在重定位操作期间它们不会政变。
▓ undefined区一对不在先前所述各个区中物件的位址参照引用都属於本区。
图3-2中是3个理想化的可重定位区的例子。这个例子使用传统的区名称:
‘.text’和‘.data’。其中水準轴表示记忆体位址。后面小节中将会详细說明ld链结器的具体操作过程。
子区
组合取得的位元组资料通常位於text或data区中。有时候在组合来源程式某个区中可能分佈著一些不相邻的资料组,但是你可以会想让它们在组合后聚集在一起存放。as组译器允许你利用子区(subsection)来达到这个目的。在每个区中,可以有编号为0- -8192的子区存在。编制在同一个子区中的物件会在目标档中与该子区中其他物件放在一起。例如,编译器可能想把常数存放在text区中,但是不想让这些常数散佈在被组合的整个程式中。在这种情況下,编译器就可以在每个会输出的代码区之前使用‘.text 0’子区,並且在每组会输出的常数之前使用‘.text 1’子区。
使用子区是可选的。如果沒有使用子区,那麼所有物件都会被放在子区0中。子区会以其从小到大的编号顺序出现在目标档中,但是目标档中並不包含表示子区的任何资讯。处理目标档的ld以及其他程式並不会看到子区的蹤跡,它们只会看到由所有text子区组成的text区;由所有data子区组成的data区。为了指定随后的语句被组合到哪个子区中,可在‘.text运算式’或‘.data运算式’中使用数值参数。运算式结果应该是絕对值。如果只指定了‘.text’,那麼就会预设使用’‘.text 0’。同樣地,‘.data’表示使用’.data 0’。
每个区都有一个位置计数器(Location Counter) ,它会对每个组合进该区的位元组进行计数。由於子区仅供as组译器使用方便而设置的,因此並不存在子区计数器。虽然沒有什麼直接操作一个位置计数器的方法,但是组合命令‘.align’可以政变其值,並且任何标号定义都会取用位置计数器的当前值。正在执行语句组合处理的区的位置计数器被称为当前活动计数器。
bss区
bss区用於储存区域公共变数。你可以在bss区中分配空间,但是在程式执行之前不能在其中放置资料。因为当程式刚开始执行时,bss区中所有位元组內容都将清除零。‘.1comm’组合命令用於在bss区中定义一个符号;‘.1comm’可用于在bss区中宣告一个公共符号。
3.2.5 符号
在程式编译和链结过程中,符号(Symbol)是一个比较重要的概念。程式师使用符号来命名物件,链结器使用符号进行链结操作,而除错器利用符号进行除错。
标号(Label)是后面紧随一个冒号的符号。此时该符号代表活动位置计数器的当前值,並且,例如,可作为指令的运算元使用。我们可以使用等号‘=’给一个符号赋予任意数值。
符号名以一个字母或‘._’字元之一开始。区域符号用於协助编译器和程式师临时使用名称。在一个程式中共有10个区域符号名(‘0’…‘9’)可供重复使用。为了定义一个区域符号,只要写出形如‘N:’的标号(其中N代表任何数字) 。若是引用前面最近定义的这个符号,需要写成‘Nb’若需引用下一个定义的区域标号,则需要写成‘Nf’。其中‘b’意思是向后(backwards) ,‘f’表示向前(forwards)。区域标号在使用方面沒有限制,但是在任何时候我们只能向前/向后引用最远l0个区域标号。
特殊点符号
特殊符号‘.’表示as组合的当前位址。因此运算式‘mylab:1ong.’就会把myIab定义为包含它自己所处的地址值。给‘.’代入就如同组合命令‘.org’的作用。因此运算式‘.=.+ 4.’与‘.space 4’完全相同。
符号属性
除了名字以外,每个符号都有“值”和“类型”属性。根据输出的格式不同,符号也可以具有辅助属性。如果不定义就使用一个符号,as就会假设其所有属性均为0。这指示该符号是一个外部定义的符号。
符号的值通常是32位元的。对於标出text、data、bss或absolute区中一个位置的符号,其值是从区开始到标号处的位址值。对於text、data和bss区,一个符号的值通常会在链结过程中由於ld政变区的基底位址而变化,absolute区中符号的值不会改变。这也是为何称它们是絕对符号的原因。
ld会对未定义符号的值进行特殊处理。如果未定义符号的值是0,则表示该符号在本组合来源程式中沒有定义,1d会尝试根据其他链结的档来确定它的值。在程式使用了一个符号但沒有对符号进行定义,就会產生这樣的符号。若未定义符号的值不为0,那麼该符号值就表示是.comm公共宣告的需要保留的公共储存空间位元组长度。符号指向该储存空间的第一个位址处。
符号的类型属性含有用於链结器和除错器的重定位资讯、指示符号号是外部的旗标以及一些其他可选资讯。对於a.out格式的目标档,符号的类型属性存放在一个8位元栏位 中(n_type位元组)。其含义请参见有关inuclude/a.out.h档的说明。
3.2.6 as组合命令
组合命令是指示组译器操作方式的虛拟指令。组合命令用於要求组译器为变数分配空间、确定程式开始地址、指定当前组合的区、修改位置计数器值等。所有组合命令的名称都以‘.’开始,其余是字元,並且大小写无关。但是通常都使用小写字元。下面我们给出一些常用组合命令的說明。
.align abs-exprl,abs-expr2,abs-expr3
.align是储存对齐组合命令,用於在当前子区中把位置计数器值设置(增加)到下一个指定储存边界处。第1个絕对值运算式abs-exprl(absolute expression)指定要求的边界对齐值。对於使用a.out格式目标档的80X86系统,该运算式值是位置计数器值增加后其二进位值最右面0值位的个数,即是2的次方值。例如‘.align 3’表示把位置计数器值增加到8的倍数上。如果位置计数器值本身就是8,那麼就无需改变。但是对於使用ELF格式的80X86系统,该运算式值直接就是要求对其的位元组数。例如‘.align 8’就是把位置计数器值增加到8的倍数上。
第2个运算式给出用於对齐而填充的位元组值。该运算式与其前面的逗号可以省略‘若省略,则填充位元组值是0。第3个可选运算式abs-expr3用於指示对齐操作允许填充跳过的最大位元组数。如果对齐操作要求跳过的位元组数大于这个最大值,那麼该对齐操作就被取消。若想省略第2个参数,可以在第l和第3个参数之间使用两个逗号。
.ascii “string ”…
从位置计数器所值当前位置为字串分配空间並储存字串。可使用逗号分开写出多个字串。例如,‘ascii “Hellow world!”,“My assembler”’。该组合命令会让as把这些字串组合在连续的位址位置处,每个字串后面不会自动添加0 (NULL)位元组。
.asciz “string”…
该组合命令与‘.ascii’类似,但是每个字串后面会自动添加NULL字元。
.byte expressions
该组合命令定义。个或多个用逗号分开的位元组值。每个运算式的值是一个位元组。
.comm symbol,length
在bss区中宣告一个命名的公共区域。在ld链结过程中,某个目标档中的一个公共符号会与其他日标档中同名的公共符号合併。如果ld沒有找到一个符号的定义,而只是一个或多个公共符号,那麼ld就会分配指定长度length位元组的未初始化记忆体。length必须足一个絕对值运算式,如果ld找到多个长度不同但同名的公共符号,ld就会分配长度最大的空间。
.data subsection
该组合命令通知as把随后的语句组合到编号为subsection的data子区中。如果省略编号,则预设使用编号0。编号必须足絕对值运算式。
.desc symbol,abs-expr
用絕对值symbol的描述符栏位 n_desc 的16位元值。 单用于a.out格式的目标档。参考有关include/a.out.h档的说明。
.fill repeat,size,value
该组合命令会产生数个(repeat个)大少为size位元组的重复复制。大少值size可以为0或某个值,但是若size大于8时,则限定为8。每个重复位元组内容取自一个8位元组数。高4位元组为0,低4位元组是数值value。这3个参数值都是绝对值,size和value 是可选的。如果第2个逗号和value省略,value预设为0值;如果后两个参数都省略的话,则size预设为1。
.global symbol (或者.globl symbol)
该组合命令会使得链结器ld能看见符号symbol。如果在我们的目标档中定义了符号symbol,那麼它的值将能被链结过程中的其他目标档使用。若目标档中沒有定义该符号,那麼它的属性将从链结过程中其他目标档的同名符号中获得。这是透过设置符号symbol类型栏位中的外部位N_EX7来做到的。参考include/a.out.h档中的說明。
.int expressions
该组合命令在某个区中设置0个或多个整数值(80386系统为4位元组,同.long)。每个用逗号分开的运算式的值就是执行时刻的值。例如.int1234,567,0x89AB 。
.lcomm symbo1,length
为符号symbol指定的区域公共区域保留长度为length位元组的空间,所在的区和符号symbol的值是新的区域公共区块的值。分配的位址在bss区中,因此在执行时刻这些位元组值被清零。由於符号symbol沒有被宣告为全域的,因此链结器ld看不见。
.long expresslons
含义与.int相同。
.octa bignums
这个组合命令指定0个或多个用逗号分开的l 6位元组大数(.byte,.word,.1ong,.quad,.octa分別对应1、2、4、8和16位元组数)。
.org new_lc,fill
这个组合命令会把当前区的位置计数器设置为值new_lc。new_lc是一个絕对值(运算式) ,或者是具有相同区作为子区的运算式,也即不能使用.org跨越各区。如果new_lc的区不对,那麼.org就不会起作用。请注意,位置计数器是基於区的,即以每个区作为计数起点。 当位置计数器值增长时,所跳跃过的位元组将被填入值fill。该值必须是絕对值。如果省略了逗号和fill,则fill预设为0值。
.quad gignums
这个组合命令指定0个或多个用逗号分开的8位元组大数bignum。如果大数放不进8个位元组中,则取最低8个位元组。
.short expressions (同.word expressions)
这个组合命令指定某个区中0个或多个用逗号分开的2位元组数。对於每个运算式,在执行时刻都会產生一个16位元的值。
.space size,fill
该组合命令產生size个位元组,每个位元组填值fill。这个参数均为絕对值。如果省略了逗号和fill,那麼fill的预设值就是0。
.string “string”
定义一个或多个用逗号分开的字串。在字串中可以使用转义字元。每个字串都自动附加一个NULL字元结尾。例如,.string “
Starting”,“other strings”。
.text subsection
通知as把随后的语句组合进编号为subsection的子区中。如果省略了编号subsection,则使用预设编号值0。
.word expressions
对於32位元机器,该组合命令含义与.short相同。
3.2.7 编写16位代码
虽然as通常用来编写纯32位元的80X86代码,但是1995年后它对编写执行於真实模式或16位元保护模式的代码也提供有限的支援。为了让as 组合时产生16位元代码,需要在执行於16位元模式的指令语句之前添加组合命令‘.codel6’並且使用组合命令‘.code32’让as组译器切換回32位元代码组合方式。
as不区分16和32位元组合语句,在16位元和32位元模式下每条指令的功能完全一樣而与模式无关。as总是为组合语句產生32位元的指令代码而不管指令将执行在16位元还足32位元模式下。如果使用组合命令‘.codel6’让as处於16位元模式下,那麼as会自动为所有指令加上一个必要的运算元宽度首码而让指令执行在16位元模式。请注意,因为as为所有指令添加了额外的位址和运算元宽度首码,所以组合產生的代码长度和性能上将会受到影响。
由於在1991年开发Linux內核0.12时as组译器还不支持16位元代码,因此在编写和组合0.12內核真实模式下的引导啟动代码和初始化组合语言程式时使用了前面介绍的as86组译器。
3.2.8 AS组译器命令行选项
-a 开啟程式列表 -f 快速操作 -o 指定输出的目标档案名 -R 组合资料区和代码区 -W 取消警告资讯
3.3 C语言程式
GNU gcc对ISO标準C89描述的C语言进行了一些扩展,其中一些扩充部分已经包括进ISO C99标準中。本节给出內核中经常用到的一些gcc扩充语句的說明。在后面章节程式注释中也会随时对遇到的扩展语句给出简单的說明。
3.3.1 C程式编译和链结
使用gcc组译器编译C语言程式时通常会经过四个处理阶段,即预处理阶段、编译阶段、组合阶段和链结阶段,见图3-3所示。
在前处理阶段中,gcc会把C程式传递给C前处理器CPP,对C语言程式中指示符号和巨集进行替換处理,输出纯C语言代码;在编译阶段,gcc把C语言程式编译生成对应的与机器相关的as组合语言代码;在组合阶段,as组译器会把组合代码转換成机器指令,並以特定二进位格式输出保存在目标档中;最后GNU ld链结器把程式的相关目标档组合链结在一起,生成程式的可执行映射档。呼叫gcc的命令行格式与编译组合语言的格式类似:
gcc [选项] [-o outfile] infile…
其中infile是输入的C语言档;outfile是编译產生的输出档。对於某次编译过程,並非一定要全部执行这四个阶段,使用命令行选项可以令gcc编译过程在某个处理阶段后就停止执行。例如,使用‘-S’选项可以让gcc在输出了C程式对应的组合语言程式之后就停止执行;使用‘-c’选项可以让gcc只生成目标档而不执行链结处理,见如下所示。
gcc -o hello hello.c //编译hello.c程式,生成执行档hello。 gcc -S -o hello.s hello.c //编译hello.c程式,生成对应组合语言程式hello.s。 gcc -c -o hello.o hello.c //编译hello.c程式,生成对应目标档hello.o而不链结。
在编译象Linux內核这樣的包含很多来源程式档的大型程式时,通常使用make工具软体对整个程式的编译过程进行自动管理,详见后面說明。
3.3.2 嵌入组合语言
本节介绍内核C语言程式中接触到的嵌入式的组合(行内组合)语句。由于我们通常编制C程式过程中一般很少用到嵌入式组合代码,因此这里有必要对其基本格式和使用方法进行说明。具有输入和输出参数的嵌入组合语句的基本格式为:
asm(“组合语句” : 输出暂存器 : 输联器 : 会被修改的暂存器);
除第l行以外,后面带冒号的行若不使用就都可以省略。其中,“asm”是行內组合语句关键字;“组合语句”是你写组合指令的地方;“输出暂存器”表示当这段嵌入组合执行完之后,哪些暂存器用於存放输出资料。此地,这些暂存器会分别对应一C语言运算式值或一个记忆体位址;“输入暂存器”表示在开始执行组合代码时,这裡指定的一些暂存器中应存放的输入值,它们也分別对应著一C变数或常数值。“会被修改的暂存器”表示你已对其中列出的暂存器中的值进行了改动,gcc编译器不能再依赖于它原先对这些暂存器载入的值。如果必要的话,gcc要重新载入这些暂存器。因此我们需要把那些沒有在输出,输入暂存器部分列出,但是在组合语句中明确使用到或隐含使用到的暂存器名列在这个部分中。
下面我们用例子来說明嵌入组合语句的使用方法。这裡列出了kernel/traps.c档中第22行开始的一段代码作为例子来详细解說。为了能看得更清楚一些,我们对这段代码进行了重新排列和编号。
01 #define get_seg_byte(se,addr) 02 ({ 03 register char __res; //定义了一个暂存器变数__res。 04 __asm__(“push %%fs; //首先保存fs暂存器原值(段选择符)。 05 mov %%ax, %%fs; //然后用seg设置fs。 06 movb %%fs:%2, %%al; //取seg :addr处l位元组內容到al暂存中。 07 pop %%fs” //恢复fs暂存器原內容。 08 : “=a” (__res) //输出暂存器列表。 09 : “0”(seg), “m” (*(addr))); //输入暂存器列表。 10 __res;})
这段l0行代码定义了一个嵌入组合语言巨集函数。通常使用组合语句最方便的方式是把它们放在一个巨集內。用小括号括住的组合语句(大括弧中的语句):({})可以作为运算式使用,其中最后一行上的变数_ _res(第10行)是该运算式的输出值,见下一节说明。
因为巨集式需要定义在一行上,因此这裡使用反斜線‘’将这些语句连成一行。这条巨集定义将被替換到程式中引用该巨集名称的地方。第l行定义了巨集的名称,也即是巨集函数名称get_seg_byte(seg,addr)。第3行定义了一个暂存器变数_res。该变数将被保存在一个暂存器中,以便於快速存取和操作。如果想指定暂存器(例如cax) ,那麼我们可以把该句写成“register char _ _res asm (“ax”);”,其中“asm”也可以写成“_ _asm_ _”。第4行上的“_ _asm_ _"表示嵌入组合语句的开始。从第4行到第7行的4条语句是AT & T格式的组合语句。另外,为了让gcc编译產生的组合语言程式中暂存器名称前有一个百分号”%”,在嵌入组合语句暂存器名称前就必须写上两个百分号”%%”。
第8行即是输出暂存器,这句的含义是在这段代码执行结束后将eax所代理的暂存器的值放入_ _reg变数中,作为本函数的输出值,“=a”中的“a”称为载入代码,”=”表示这是输出暂存器,並且其中的值将被输出值替代。第9行表示在这段代码开始执行时将seg放到eax暂存器中,“0”表示使用与上面同个位置的输出相同的暂存器,而(*(addr))表示一个记忆体偏移位址值。为了在上面组合语句中使用该位址值,嵌入组合语言程式规定把输出和输入暂存器统一按顺序编号,顺序是从输出暂存器序列从左到右从上到下以”%0开始、分別记为%0、%l、…%9。因此,输出暂存器的编号是%0(这裡只有一个输出暂存器) ,输入暂存器前一部分(”0”(seg))的编号是%1,而后部分的编号是%2 。上面第6行月的%2即代表(*(addr))这个记忆体偏移量。
现在我们来研究4--7行上的代码作用。第一句将fs段暂存器的內容入堆疊; 第二句将eax中的段值代入fs段暂存器;第三句是把fs :(*(addr))所指定的位元组放入al暂存器中。当执行完组合语句后,输出暂存器eax的值将被放入_ _res, 作为该巨集函数(区块结构运算式)的返回值。很简单,不是吗?
透过上面分析,我们知道,巨集名称中的seg代表一指定的记忆体段值,而addr表示一记忆体偏移位址量。到现在为止,我们应该很清楚这段程式的功能了吧! 该巨集函数的功能是从指定段和偏栘值的记忆体位址处取一个位元组。再在看下一个例子。
l. 3行这三句是平常的组合语句,用以清方向位,重复保存值。第4行說明这段嵌入组合语言程式沒有用到输出暂存器。第5行的含义是:将count-l的值载入到ecx暂存器中(载入代码是”C”),fill_value载入到eax中,dest放到edi中。为什麼要让gcc编译程序去做这樣的暂存器值的载入,而不让我们自己做呢? 因为gcc在它进行暂存器分配时可以进行某些最佳化工作,例如fill_value值可能已经在eax中。如果是在一个回圈语句中的话,gcc就可能在整个回圈操作中保留eax,这樣就可以在每次回圈中少用一个movl语句。
最后一行的作用是告诉gcc这些暂存器中的值已经改变了,在gcc知道你拿这些暂存器做些什麼后,能夠对gcc的最佳化操作有所帮助。表3-4中是一些你可能会用到的暂存器载入代码及其具体的含义。
下面的例子不是让你自己指定哪个变数使用哪个暂存器,而是让gcc为你选择。
01 asm(“leal (%1,%1,4),%0 02 :“=r”(y) 03 : “0” (x));
指令"leal"用於计算有效位址,但这裡用它来进行一些简单计算。第l条句组合语句"leal(rl,r2,4),r3"语句表示rl+r2*4 →r3。这个例子可以非常快地将x乘5。 其中“%0”、“%l”是指gcc自动分配的暂存器。这裡“%l”代表输入x要放入的暂存器,“%0”表示输出值暂存器。输出暂存器代码前一定要加等于号。如果输入暂存器的代码是0或为空时,则說明使用与相应输出一樣的暂存器。所以,如果gcc将r指定为eax的话,那麼上面组合语句的含义即为:
“leal (eax,eax,4),eax”
注意:在执行代码时,如果不希望组合语句被GCC最佳化而作修改,就需要在asm符号后面添加关键字volatile,见下面所示。这两种宣告的区別在於程式相容性方面。建议使用后一种宣告方式。
asm volatile (┉); 或者更详细的說明为: __asm__ __volatile__(┉);
关键字volatile也可以放在函数名前来修饰函数,用来通知gcc编译器该函数不会返回。这樣就可以让gcc產生更好一些的代码。另外,对於不会返回的函数,这个关键字也可以用来避免gcc產生假警告资讯。例如mm/memory.c中的如下语句說明函数do_exit( )和oom( )不会再返回到呼叫者代码中:
下面的例子不是让你自己指定哪个变数使用哪个暂存器,而是让gcc为你选择。
01 asm(“leal (%1,%1,4),%0 02 :“=r”(y) 03 : “0” (x));
指令"leal"用於计算有效位址,但这裡用它来进行一些简单计算。第l条句组合语句"leal(rl,r2,4),r3"语句表示rl+r2*4 →r3。这个例子可以非常快地将x乘5。 其中“%0”、“%l”是指gcc自动分配的暂存器。这裡“%l”代表输入x要放入的暂存器,“%0”表示输出值暂存器。输出暂存器代码前一定要加等于号。如果输入暂存器的代码是0或为空时,则說明使用与相应输出一樣的暂存器。所以,如果gcc将r指定为eax的话,那麼上面组合语句的含义即为:
“leal (eax,eax,4),eax”
注意:在执行代码时,如果不希望组合语句被GCC最佳化而作修改,就需要在asm符号后面添加关键字volatile,见下面所示。这两种宣告的区別在於程式相容性方面。建议使用后一种宣告方式。
asm volatile (┉); 或者更详细的說明为: __asm__ __volatile__(┉);
关键字volatile也可以放在函数名前来修饰函数,用来通知gcc编译器该函数不会返回。这樣就可以让gcc產生更好一些的代码。另外,对於不会返回的函数,这个关键字也可以用来避免gcc產生假警告资讯。例如mm/memory.c中的如下语句說明函数do_exit( )和oom( )不会再返回到呼叫者代码中:
下面再例举一个较长的例子,如果能看得懂,那就說明嵌入组合代码对你来說基本沒问题了。这段代码是从include/string.h档中摘取的,是strncmp( )字串比较函数的一种实现。需要注意的是,其中每行中的“ ”是用於gcc预处理程式输出列表好看而设置的,含义与C语言中相同。
注译:
Stack - 堆叠 或者称呼为 堆栈 Segment - 段 Flag - 旗标 或者称呼为 标志 暂存器又称呼为寄存器
To be continued.........
|