标签 阅读笔记 下的文章
最近这段时间一直在学习minix 文件系统1.0的内容,想着在Windows上通过自己编写的软件直接将文件传输到Linux 0.11的硬盘上(功能类似于FTP工具),这样就不需要自己搭一个虚拟机这么麻烦了,而且文件交互更方便,目前完成了基础功能,等完全弄好之后,会在我的博客上进行发布的,敬请关注。下面进入本文的内容。Makefile(或makefile)文件是make工具程序的配置文件。make工具程序的主要用途是能自动的决定一个含有很多源程序文件的大型程序中哪个文件需要被重新编译。Makefile的使用比较复杂,这里只作简单的介绍,详细说明请参考GNU make使用手册。Makefile用于告诉make要做些什么工作。make的执行过程分为两个不同的阶段。在第一个阶段,它读取所有的Makefile文件以及包含的Makefile文件等,记录所有的变量及其值、隐式的或显式的规则,并构造出所有目标对象及其先决条件的一幅全景图。在第二阶段期间,make就使用这些内部结构来确定哪个目标对象需要被重建,并且使用相应的规则来操作。当make重新编译程序时,每个修改过的C代码文件必须被重新编译。如果一个头文件被修改过了,那么为了确保正确,每一个包含该头文件的C代码程序都将被重新编译。每次编译操作都产生一个与源程序对应的目标文件。最终,如果任何源代码文件被编译过了,那么所有的目标文件不管是刚编译完的还是以前就编译好的必须连接在一起以生成新的可执行文件。简单的Makefile文件含有一些规则,这些规则具有如下形式:目标(target)…:先决条件(prerequisites)… 命令(command) ... ...其中’目标’对象通常是程序生成的一个文件的名称,例如是一个可执行文件或目标文件。目标也可以是所要采取活动的名字,比如’清除’(clean)。’先决条件’是一个或多个文件名,是用作产生目标的输入条件。通常一个目标依赖几个文件。而’命令’是make需要执行的操作。一个规则可以有多个命令,每一个命令自成一行。注意:你需要在每个命令行之前键入一个制表符(也就是tab)!自动变量是一种在命令行上根据具体情况能被自动替换的变量。自动变量的值是基于目标对象及其先决条件而在命令执行前设置的。这里列举一些比较常用的自动变量:$^:表示规则的所有先决条件,包括它们所处目录的名称;$<:表示规则中的第一个先决条件;$@:表示目标对象。
为了生成内核代码文件,Linux 0.11使用了两种编译器。第一种是汇编编译器as86和相应的链接程序(或称为链接器)ld86。它们专门用于编译和链接,运行在实地址模式下的16位内核引导扇区程序bootsect.s和设置程序setup.s。第二种是GNU的汇编器as(gas)和C语言编译器gcc以及相应的链接程序ld。编译器用于为源程序文件产生对应的二进制代码和数据目标文件。链接程序用于对相关的所有目标文件进行组合处理,形成一个可被内核加载执行的目标文件,即可执行文件。1、目标文件格式在Linux 0.11系统中,GNU gcc或as编译输出的目标模块文件和链接程序所生成的可执行文件都使用了UNIX传统的a.out格式。目标文件格式示意图如下:a.out格式7个区的基本定义和用途如下所述:执行头部分(exec header):该部分中含有一些参数(exec结构),是有关目标文件的整体结构信息。例如代码和数据区的长度、未初始化数据区的长度、对应源程序文件名以及目标文件创建时间等。内核使用这些参数把执行文件加载到内存中并执行,而链接程序ld使用这些参数将一些模块文件组合成一个可执行文件。这是目标文件唯一必要的组成部分。代码区(text segment):由编译器或汇编器生成的二进制指令代码和数据信息,含有程序执行时被加载到内存中的指令代码和相关数据。可以以只读形式被加载。数据区(data segment):由编译器或汇编器生成的二进制指令代码和数据信息,这部分含有已经初始化过的数据,数据区总是被加载到可读写的内存中。代码重定位部分(text relocations):这部分含有供链接程序使用的记录数据。在组合目标模块文件时,用于定位代码段中的指针或地址。当链接程序需要改变目标代码的地址时,就需要修正和维护这些地方。数据重定位部分(data relocations):类似于代码重定位部分的作用,但是用于数据段中指针的重定位。符号表部分(symbol table):这部分同样含有供链接程序使用的记录数据。这些记录数据保存着模块文件中定义的全局符号以及需要从其他模块文件中输入的符号,或者是由链接器定义的符号,用于在模块文件之间对命名的变量和函数(符号)进行交叉引用。字符串表部分(string table):该部分含有与符号名相对应的字符串。用于调试程序,调试目标代码,与链接程序无关。这些信息可包含源程序代码和行号、局部符号以及数据结构描述信息等。目标文件的文件头中含有一个长度为32字节的exec数据结构,通常称为文件头结构或者执行头结构。有关a.out结构的详细信息请参见include/a.out.h文件后的介绍。文件头结构定义如下:根据a.out文件中头结构魔数字段的值,我们又可把a.out格式的文件分成几种类型。Linux 0.11系统使用了其中两种类型:模块目标文件使用了OMAGIC(Old Magic)类型的a.out格式,它指明文件是目标文件或者是不纯的可执行文件,其魔数是0x107(八进制0407)。而执行文件使用了ZMAGIC类型的a.out格式,它指明文件为需求分页处理(demang-paging,即需求加载load on demand)的可执行文件,其魔数是0x10b(八进制0413)。这两种格式的主要区别在于它们对各个部分的存储分配方式上。虽然该结构的总长度只有32字节,但是对于一个ZMAGIC类型的执行文件来说,其文件开始部分却需要专门留出1024字节的空间给头结构使用。除头结构占用的32个字节以外,其余部分均为0。从1024字节之后才开始放置程序的正文段和数据段等信息。而对于一个OMAGIC类型的.o模块文件来说,文件开始部分的32字节头结构后面紧接着就是代码区和数据区。a_entry字段指定了程序代码开始执行的地址。而a_syms、a_trsize和a_drsize字段则分别说明了数据段后符号表、代码和数据段重定位信息的大小。对于可执行文件来说并不需要符号表和重定位信息,因此除非链接程序为了调试目的而包括了符号信息,执行文件中的这几个字段的值通常为0。Linux 0.11系统的模块文件和执行文件都是a.out格式的目标文件,但是只有编译器生成的模块文件中包含用于链接程序的重定位信息。代码段和数据段的重定位信息均由重定位记录项构成,每个记录的长度为8字节,其结构如下:重定位项的功能有两个。一是当代码段被重定位到一个不同的基地址处时,重定位项则用于指出需要修改的地方。二是在模块文件中存在对未定义符号引用时,当此未定义符号最终被定义时链接程序就可以使用相应重定位项对符号的值进行修正。目标文件的最后部分是符号表和相关的字符串表。符号表记录项的结构如下:2、Linux 0.11中的目标文件格式磁盘上a.out执行文件的各区在进程逻辑地址空间中的对应关系如下图所示:Linux 0.11系统中进程的逻辑地址空间大小是64MB。对于ZMAGIC类型的a.out执行文件,它的代码区的长度是内存页面的整数倍。由于Linux 0.11内核使用需求页(Demand-paging)技术,即在一页代码实际要使用的时候才被加载到物理内存页面中,而在进行加载操作的fs/execve()函数中仅仅为其设置了分页机制的页目录项和页表项,因此需求页技术可以加快程序的加载速度。图中bss是进程的未初始化数据区,用于存放静态的未初始化数据。在开始执行程序时,bss的第一页内存会被设置为全0。图中heap是堆空间区,用于分配进程在执行过程中动态申请的内存空间。3、链接程序输出链接程序对输入的一个或多个模块文件以及相关的库函数模块进行处理,最终生成相应的二进制执行文件或者是一个所有模块组合而成的大模块文件。对于a.out格式的模块文件来说,由于段类型是预先知道的,因此链接程序对a.out格式的模块文件进行存储分配比较容易。例如,对于具有两个输入模块文件和需要链接一个库函数模块的情况,其存储分配情况如下图所示:4、链接程序预定义变量在链接过程中,链接器ld和ld86会使用变量记录下执行程序中每个段的逻辑地址。因此在程序中可以通过访问这几个外部变量来获得程序中段的位置。链接器预定义的外部变量通常至少有etext、_etext、edata、_edata、end和_end。变量名etext和_etext的地址是程序正文段结束后的第1个地址;edata和_edata的地址是初始化数据区后面的第1个地址;end和_end的地址是未初始化数据区(bss)后的第1个地址位置。带下划线’_’前缀的名称等同于不带下划线的对应名称,它们之间的唯一区别在于ANSI、POSIX等标准中没有定义符号etext、edata和end。5、System.map文件在编译内核时,linux/Makefile文件产生的System.map文件就用于存放内核符号表信息。当内核运行出错时,通过System.map文件中的符号表解析,就可以查到一个地址值对应的变量名,或反之。尽管内核本身实际上不使用System.map,但其他程序,像klogd、lsof、ps以及其他像dosemu等许多软件都需要一个正确的System.map文件。利用该文件,这些程序就可以根据已知的内存地址查找出对应的内核变量名称,便于对内核的调试工作。
1、C函数调用机制函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移。数据传递通过函数参数和返回值来进行。另外,我们还需要在进入函数时为函数的局部变量分配存储空间,并且在退出函数时收回这部分空间。Intel 80x86 CPU为控制传递提供了简单的指令,而数据的传递和局部变量存储空间的分配与回收则通过栈操作来实现。(1)、栈帧结构和控制转移权方式大多数CPU上的程序实现使用栈来支持函数调用操作。栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复以及用来存储局部数据。单个函数调用操作所使用的栈部分被称为栈帧(Stack frame)结构,结构如图3-4所示。栈帧结构的两端由两个指针来指定。寄存器ebp通常用作帧指针(frame pointer),而esp则用作栈指针(stack pointer)。在函数执行过程中,栈指针esp会随着数据的入栈和出栈而移动,因此函数中对大部分数据的访问都基于帧指针ebp进行。对于函数A调用函数B的情况,传递给B的参数包含在A的栈帧中。当A调用B时,函数A的返回地址(调用返回后继续执行的指令地址)被压入栈中,栈中该位置也明确指明了A栈帧的结束处。而B的栈帧则从随后的栈部分开始,即图中保存栈指针(ebp)的地方开始。再随后则用于存放任何保存的寄存器值以及函数的临时值。栈是往低(小)地址方向扩展的,而esp指向当前栈顶处的元素。通过使用push和pop指令我们可以把数据压入栈中或者从栈中弹出。对于没有指定初始值的数据所需要的存储空间,我们可以通过把栈指针递减适当的值来做到。类似的,通过增加栈指针值我们可以回收栈中已分配的空间。指令CALL和RET用于处理函数调用和返回操作。调用指令CALL的作用是把返回地址压入栈中并且跳转到被调用函数开始处执行。返回地址是程序中紧随调用指令CALL后面一条指令的地址。因此当被调函数返回时就会从该位置继续执行。返回指令RET用于弹出栈顶处的地址并跳转到该地址处。在使用该指令之前,应该先正确处理栈中内容,使得当前栈指针所指位置内容正是先前CALL指令保存的返回地址。另外,若返回值是一个整数或者一个指针,那么寄存器eax将被默认用来传递返回值。尽管某一时刻只有一个函数在执行,但我们还是需要确定在一个函数(调用者)调用其他函数(被调用者)时,被调用者不会修改或覆盖掉调用者今后要用到的寄存器内容。因此Intel CPU采用了所有函数必须遵守的寄存器用法统一惯例。该惯例指明,寄存器eax、edx和ecx的内容必须由调用者自己负责保存。当函数B被A调用时,函数B可以在不用保存这些寄存器内容的情况下任意使用它们而不会毁坏函数A所需要的任何数据。另外,寄存器ebx、esi和edi的内容则必须由被调用者B来保护。当被调用者需要使用这些寄存器中的任何一个时,必须首先在栈中保存其内容,并在退出时恢复这些寄存器的内容。因为调用者A(或者一些更高层的函数)并不负责保存这些寄存器的内容,但可能在以后的操作中还需要用到原先的值。还有寄存器ebp和esp也必须遵守第二个惯例用法,即由被调用者来保护。(2)、函数调用举例我们观察下面C程序exch.c中函数调用的处理过程。该程序交换两个变量中的值,并返回它们的差值:其中函数swap()用于交换两个变量的值。C程序中的主程序main()也是一个函数(将在本章的后面进行说明),它在调用了swap()之后返回交换的结果。这两个函数的栈帧结构如图3-5所示。可以看出,函数swap()从调用者(main())的栈帧中获取其参数。图中的位置信息相对于寄存器ebp中的帧指针。栈帧左边的数字指出了相对于帧指针的地址偏移值。在象gdb这样的调试器中,这些数值都用2的补码表示,例如,’-4’被表示成’0xfffffffc’,’-12’被表示成’0xfffffff4’。调用者main()的栈帧结构中,包括局部变量a和b的存储空间,相对于帧指针位于-4和-8偏移处。由于我们需要为这两个局部变量生成地址,因此它们必须保存在栈中而非简单的存放在寄存器中。使用命令”gcc -Wall -S -o exch.s exch.c”可以生成该C语言程序的汇编程序exch.s代码,见如下所示(删除了一些无关的代码):.text_swap: pushl %ebp #保存原ebp值,设置当前函数的帧指针 movl %esp, %ebp subl $4, %esp #为局部变量c在栈内分配空间 movl 8(%ebp), %eax #取函数第1个参数,该参数是一个整数类型值的指针 movl (%eax),%ecx #取该指针所指位置的内容,并保存到局部变量c中 movl %ecx, -4(%ebp) movl 8(%ebp), %eax #再次取第1个参数,然后取第2个参数 movl 12(%ebp), %edx movl (%edx), %ecx #把第2个参数所指内容放到第1个参数所指的位置 movl %ecx, (%eax) movl 12(%ebp), %eax #再次取第2个参数 movl -4(%ebp), %ecx #然后把局部变量c中的内容放到这个指针所指位置处 movl %ecx, (%eax) leave #恢复原ebp、esp值(即movl %ebp,%esp; popl %ebp) ret_main: pushl %ebp #保存原ebp值,设置当前函数的帧指针 movl %esp, %ebp subl $8, %esp #为整型局部变量a和b在栈中分配空间 movl $16, -4(%ebp) #为局部变量赋初值(a=16,b=32) movl $32, -8(%ebp) leal -8(%ebp), %eax #为调用swap()函数作准备,取局部变量b的地址 pushl %eax #作为调用的参数并压入栈中,即先压入第2个参数 leal -4(%ebp), %eax #再取局部变量a的地址,作为第1个参数入栈 pushl %eax call _swap #再调用函数swap() movl -4(%ebp), %eax #取第一个局部变量a的值,减去第二个变量b的值 subl -8(%ebp), %eax leave #恢复原ebp、esp值(即movl %ebp,%esp; popl %ebp) ret这两个函数均可以划分为3个部分:”设置”,初始化栈帧结构;”主体”,执行函数的实际计算操作;”结束”,恢复栈状态并从函数中返回。leave指令用于处理栈内容以准备返回,它的作用等价于下面两个指令:movl %ebp, %esp#恢复原esp的值(指向栈帧开始处)popl %ebp#恢复原ebp的值(通常是调用者的帧指针)变量地址压入栈中的顺序正好与函数声明的参数顺序相反。即函数最后一个参数首先压入栈中,而函数的第1个参数则是最后一个在调用函数指令call之前压入栈中的。(3)、main也是一个函数上面提到C程序的主程序main()也是一个函数,这是因为在编译链接时它将会作为crt0.s汇编程序的函数被调用。crt0.s是一个桩(stub)程序,名称中的”crt”是”C run-time”的缩写。该程序的目标文件将被链接在每个用户执行程序的开始部分,主要用于设置一些初始化全局变量等。Linux 0.11中crt0.s汇编程序如下所示,其中建立并初始化全局变量_environ供程序中其他模块使用。.text.globl _environ #声明全局变量_environ(对应C程序中的environ变量)__entry:#代码入口标号 movl 8(%esp), %eax #取程序的环境变量指针envp并保存在_environ中 movl %eax, _environ #envp是execve()函数在加载执行文件时设置的 call _main #调用我们的主程序,其返回状态值在eax寄存器中 pushl %eax #压入返回值作为exit()函数的参数并调用该函数1:call _exit jmp 1b #控制应该不会到达这里,若到达这里则继续执行exit().data_environ: #定义变量_environ,为其分配一个长字空间 .long 0在通常的编译过程中,我们无需特别指定stub模块crt0.o,但是若想从上面给出的汇编程序手工使用ld从exch.o模块链接产生可执行文件exch,那么我们就需要在命令行上特别指明crt0.o这个模块,并且链接的顺序应该是”crt0.o、所有程序模块、库文件”。为了使用ELF格式的目标文件以及建立共享库模块文件,现在的gcc编译器(2.x)已经把这个crt0扩展成几个模块:crt1.0、crti.o、crtbegin.o、crtend.o和crtn.o。这些模块的链接顺序为“crt1.0、crti.o、crtbegin.o(crtbeginS.o)、所有程序模块、crtend.o(crtendS.o)、crtn.o、库模块文件”。gcc的配置文件specfile指定了这种链接顺序。其中crt1.o、crti.o和crtn.o由C库提供,是C程序的“启动”模块;crtbegin.o和crtend.o是C++语言的启动模块,由编译器gcc提供;而crt1.o则与crt0.o的作用类似,主要用于在调用main()之前做一些初始化工作,全局符号_start就定义在这个模块中。crtbegin.o和crtend.o主要用于C++语言在.ctors和.dtors区中执行全局构造器(constructor)和析构器(destructor)函数。crtbeginS.o和crtendS.o的作用与前两者类似,但用于创建共享模块中。crti.o用于在.init区中执行初始化函数init()。.init区中包含进程的初始化代码,即当程序开始执行时,系统会在调用main()之前先执行.init中的代码。crtn.o则用于在.fini区中执行进程终止退出处理函数fini()函数,即当程序正常退出时(main()返回之后),系统会安排执行.fini中的代码。boot/head.s程序中第136-140行就是用于为跳转到init/main.c中的main()函数作准备工作。第139行上的指令在栈中压入了返回地址,而第140行则压入了main()函数代码的地址。当head.s最后在第218行上执行ret指令时就会弹出main()的地址,并把控制权转移到init/main.c程序中。2、在汇编程序中调用C函数在汇编程序调用一个C函数时,程序需要首先按照逆向顺序把函数参数压入栈中,即函数最后(最右边的)一个参数先入栈,而最左边的第1个参数在最后调用指令之前入栈,如图3-6所示。然后执行CALL指令去执行被调用的函数。在调用函数返回后,程序需要再把先前压入栈中的函数参数清除掉。在执行CALL指令时,CPU会把CALL指令下一条指令的地址压入栈中(见图中EIP)。如果调用还涉及到代码特权级变化,那么CPU还会进行堆栈切换,并且把当前堆栈指针、段描述符和调用参数压入新堆栈中。由于Linux内核中只使用中断门和陷阱门方式处理特权级变化时的调用情况,并没有使用CALL指令来处理特权级变化的情况,因此这里对特权级变化时的CALL指令使用方式不再进行说明。汇编中调用C函数比较“自由”。只要是在栈中适当位置的内容就都可以作为参数供C函数使用。这里以图3-6中具有3个参数的函数调用为例,如果我们没有专门为调用函数func()压入参数就直接调用它的话,那么func()函数仍然会把存放EIP位置以上的栈中其他内容作为自己的参数调用。如果我们为调用func()而仅仅明确的压入了第1、第2个参数,那么func()函数的第3个参数p3就会直接使用p2前的栈中内容。另外,我们说汇编程序调用C函数比较自由的另一个原因是我们可以根本不用CALL指令而采用JMP指令来同样达到调用函数的目的。方法是在参数入栈后人工把下一条要执行的指令地址压入栈中,然后直接使用JMP指令跳转到被调用函数开始地址处去执行函数。此后,当函数执行完成时就会执行RET指令把我们人工压入栈中的下一条指令地址弹出,作为函数返回的地址。Linux内核中也有多处用到了这种函数调用方法,例如kernel/asm.s程序第62行调用执行traps.c中的do_int3()函数的情况。3、在C程序中调用汇编函数从C程序中调用汇编程序函数的方法与汇编程序中调用C函数的原理相同,但Linux内核程序中不常使用。调用方法的着重点仍然是对函数参数在栈中位置的确定上。当然,如果调用的汇编语言程序比较短,那么可以直接在C程序中使用前面介绍的内联汇编语句来实现。下面是一个示例,包含两个函数的汇编程序callee.s如下所示:#/*# *By:Ailson Jack# *Date:2018.08.28# *Blog:www.only2fire.com# *Des:本汇编程序利用系统调用sys_write()实现显示函数int mywrite(int fd, char *buf, int count).# * 函数int myadd(int a, int b, int *res)用于执行a+b=res运算.若函数返回0,则说明溢出.# *注意:如果在现在的Linux系统(例如RedHat9)下编译,则请去掉函数名前的下划线'_'.# *编译:as -o callee.o callee.s#*/#.code32 #让as汇编器切换为32位代码汇编方式,否则在64位系统下编译时,编译的时候可能会出现"invalid instruction suffix for push"的问题SYSWRITE = 4 #sys_write()系统调用号.globl _mywrite, _myadd.text_mywrite: pushl%ebp movl%esp, %ebp pushl%ebx movl8(%ebp), %ebx #取调用者的第1个参数:文件描述符 fd movl12(%ebp), %ecx #取第2个参数:缓冲区指针 movl16(%ebp), %edx #取第3个参数:显示字符数 movl$SYSWRITE, %eax # %eax中放入系统调用号4 int $0x80 #执行系统调用 popl%ebx movl%ebp, %esp popl%ebp ret_myadd: pushl%ebp movl%esp, %ebp movl8(%ebp), %eax #取第1个参数:a movl12(%ebp), %edx #取第2个参数:b xorl%ecx, %ecx # %ecx为0表示计算溢出 addl%eax, %edx #执行加法运算 jo 1f #若溢出 则跳转 movl16(%ebp), %eax #取出第3个参数的指针 movl%edx, (%eax) #把计算结果放入指针所指位置处 incl%ecx #没有发生溢出,于是设置无溢出返回值1:movl%ecx, %eax # %eax中是函数返回值 movl%ebp, %esp popl%ebp ret该汇编文件中的第1个函数mywrite()利用系统中断0x80调用系统调用sys_write(int fd, char *buf, int count)实现在屏幕上显示信息。对应的系统调用功能号是4(参见include/unistd.h中的__NR_write),三个参数分别为文件描述符、显示缓冲区指针和显示字符数。在执行int 0x80之前,寄存器%eax中需要放入调用功能号(4),寄存器%ebx、%ecx和%edx要按调用规定分别存放fd、buf和count。函数mywrite()的调用参数个数和用途与sys_write()完全一样。第2个函数myadd(int a, int b, int *res)执行加法运算,其中参数res是运算的结果。函数返回值用于判断是否发生溢出。如果返回值为0表示计算已经发生溢出,结果不可用。否则计算结果将通过res返回给调用者。注意:如果在现在的Linux系统(例如RedHat 9)下编译callee.s程序,则请去掉函数名前的下划线’_’。调用这两个函数的C程序caller.c如下所示:该函数首先利用汇编函数mywrite()在屏幕上显示开始计算的信息“Calculating…”,然后调用加法计算汇编函数myadd()对a和b两个数进行运算,并在第3个参数res中返回计算结果。最后再利用mywrite()函数把格式化过的结果信息字符串显示在屏幕上。如果函数myadd()返回0,则表示加函数发生溢出,计算结果无效。这两个文件的编译和运行结果如下所示:# as -o callee.o callee.s# gcc -o caller caller.c callee.o# ./caller注意:上述汇编程序和C程序在我提供的Linux 0.11系统和Ubuntu 14.04的32位系统均可以编译和运行成功;在Ubuntu 14.04的64位系统中可以编译,但是运行不成功。在其他的32位版本Linux系统中应该也可以正常的编译和运行,64位系统可能不行。 上述代码例子我已经上传到云盘,大家可以自行下载研究,下载地址:点此下载 密钥:9gdm。
本小节给出内核中经常用到的一些gcc扩充语句的说明。1、C程序编译和链接使用gcc编译器编译C语言程序时,通常会经过4个处理阶段,即预处理阶段、编译阶段、汇编阶段和链接阶段,如下图所示:gcc的命令行格式如下:gcc [选项] [-o outfile] infile…infile是输入的C语言文件,outfile是编译产生的输出文件。对于某次编译过程,并非一定要全部执行这4个阶段,使用命令行选项可以让gcc编译过程在某个处理阶段后就停止执行。例如,使用’-S’选项可以让gcc在输出了C程序对应的汇编语言程序之后就停止运行;使用’-c’选项可以让gcc只生成目标文件(.o后缀的文件)而不执行链接处理。2、嵌入汇编本节介绍内核C语言程序中接触到的嵌入式汇编(内联汇编)语句。具有输入和输出参数的嵌入式汇编语句的基本格式如下:asm(“汇编语句” : 输出寄存器 : 输入寄存器 : 会被修改的寄存器);除第1行以外,后面带冒号的行若不使用就都可以省略。其中,”asm”是内联汇编语句的关键词;”汇编语句”是你写汇编指令的地方。”输出寄存器”表示当这段嵌入式汇编执行完之后,哪些寄存器用来存放输出数据,这些寄存器会分别对应一C语言表达式值或一个内存地址。”输入寄存器”表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一C变量或常数值。”会被修改的寄存器”表示你已对其中列出的寄存器中的值进行了改动,gcc编译器不能再依赖于它原先对这些寄存器加载的值。如果必要的话,gcc需要重新加载这些寄存器。因此我们需要把那些没有在输出/输入寄存器部分列出,但是在汇编语句中明确使用到或隐含使用到的寄存器名列在这部分。下面列出了kernel/traps.c文件中第22行开始的一段代码作为例子来进行说明:#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;})这段10行代码定义了一个嵌入式汇编语言宏函数。通常使用汇编语句最方便的方式是把它们放在一个宏内。用圆括号括住的组合语句(花括号中的语句):”({})”可以作为表达式使用,其中最后一行上的变量__res(第10行)是该表达式的输出值,见下一节说明。因为宏语句需要在一行上,因此这里使用反斜杠’\’将这些语句连接成一行。这条宏定义将被替换到程序中引用该宏名称的地方。第1行定义了宏名称,也即是宏函数名称get_seg_byte(seg,addr)。第3行定义了一个寄存器变量__res,该变量将被保存在一个寄存器中,以便于快速访问和操作。如果想指定寄存器(例如eax),那么我们可以把该句写成”register char __res asm(“ax”);”,其中’asm’也可以写成’__asm__’。第4行上的’__asm__’表示嵌入汇编语句的开始。从第4行到第7行的4条语句是AT&T格式的汇编语句。另外,为了让gcc编译产生的汇编语言程序中寄存器名称前有一个百分号”%”,在嵌入式汇编语句寄存器名称前就必须写上两个百分号”%%”。第8行即是输出寄存器,这句的含义是在这段代码运行结束后将eax所代表的寄存器的值放入__res变量中,作为本函数的输出值。”=a”中的”a”称为加载代码,”=”表示这是输出寄存器,并且其中的值将被输出值替代。第9行表示在这段代码开始运行时,将seg放到eax寄存器中,”0”表示使用与上面同个位置的输出相同的寄存器。而(*(addr))表示一个内存偏移地址值。为了在上面汇编语句中使用该地址值,嵌入汇编程序规定把输出和输入寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以”%0”开始,分别记为%0、%1、…、%9。因此,输出寄存器的编号是%0(这里只有一个输出寄存器),输入寄存器前一部分(“0”(seg))的编号是%1,而后部分的编号是%2。上面第6行上的%2即代表(*(addr))这个内存偏移量。该宏函数的功能是从指定段和偏移值的内存地址处取一个字节。下面在看一个例子:asm(“cld\n\t” “rep\n\t” “stol” : /*没有输出寄存器*/ : “c”(count-1), “a”(fill_value), “D”(dest) : “%ecx”, “edi%”);第4行说明这段嵌入汇编程序没有用到输出寄存器。第5行的含义是:将count-1的值加载到ecx寄存器中(加载代码是”c”),fill_value加载到eax中,dest放到edi中。表3-4中是一些你可能会用到的寄存器加载代码及其具体的含义:下面的例子不是让你自己指定哪个变量使用哪个寄存器,而是让gcc为你选择。asm(“leal (%1,%1,4), %0” : “=r”(y) : “0”(x));“leal (r1, r2, 4), r3”语句表示:(r1+r2*4) -> r3。如果gcc将r指定为eax的话,那么上面汇编语句的含义为:“leal (eax,eax,4), eax”注意:在执行代码时,如果不希望汇编语句被gcc优化而作修改,就需要在asm符号后面添加关键词volatile,下面列出了两种声明方式:asm volatile (...);__asm__ __volatile__ (...); //建议使用这种方式,兼容性更好关键词volatile也可以放在函数名前来修饰函数,用来通知gcc编译器该函数不会返回,即该函数不会返回到调用者代码中(例如do_exit()函数)。下面看一个比较长的例子,该代码是从include/string.h文件中摘取的,是strncmp()字符串比较函数的一种实现。其中每行中的”\n\t”是用于gcc预处理程序输出列表好看而设置的。//字符串1和字符串2的前count个字符进行比较//参数: cs:字符串1, ct:字符串2, count:比较的字符数//%0:eax(__res)返回值, %1:edi(cs)串1指针, %2:esi(ct)串2指针, %3:ecx(count)//返回:如果串1>串2,则返回1; 串1==串2,则返回0; 串1<串2,则返回-1extern inline int strncmp(const char * cs,const char * ct,int count){register int __res __asm__("ax"); //__res是寄存器变量,该变量保存在寄存器eax中__asm__("cld\n" //清方向位 "1:\tdecl %3\n\t" //count-- "js 2f\n\t" //如果count<0,则向前跳转到标号2 "lodsb\n\t" //取串2的字符ds:[esi]->al,并且esi++ "scasb\n\t" //比较al与串1的字符es:[edi],并且edi++ "jne 3f\n\t" //如果不相等,则向前跳转到标号3 "testb %%al,%%al\n\t" //该字符是NULL字符吗? "jne 1b\n" //不是,则向后跳转到标号1,继续比较 "2:\txorl %%eax,%%eax\n\t" //是NULL字符,则eax清零(返回值) "jmp 4f\n" //向前跳转到标号4,结束 "3:\tmovl $1,%%eax\n\t" //eax中置1 "jl 4f\n\t" //如果前面比较中,串2字符<串1字符,则返回1,结束 "negl %%eax\n" //否则eax=-eax,返回负值,结束 "4:" :"=a" (__res):"D" (cs),"S" (ct),"c" (count):"si","di","cx");return __res; //返回比较结果}3、圆括号中的组合语句花括号对”{...}”用于把变量声明和语句组合成一个复合语句(组合语句)或一个语句块,这样在语义上这些语句就等同于一条语句,组合语句的右花括号后面不需要使用分号。圆括号中的组合语句,即形如”({...})”的语句,可以在GNU C中用作一个表达式使用。这样就可以在表达式中使用loop、switch语句和局部变量,因此这种形式的语句通常称为语句表达式。语句表达式具有如下示例的形式:({int y = foo(); int z; if(y > 0) z = y; else z = -y; 3 + z;})其中组合语句中最后一条语句必须是后面跟随一个分号的表达式。这个表达式(“3+z”)的值即用作整个圆括号括住语句的值。如果最后一条语句不是表达式,那么整个语句表达式就具有void类型,因此没有值。这种表达式中语句声明的任何局部变量都会在整个语句结束后失效。4、寄存器变量GNU 对C语言的另一个扩充是允许我们把一些变量值放到CPU寄存器中,即所谓的寄存器变量。寄存器变量可以分为2种:全局寄存器变量和局部寄存器变量。在GNU C程序中我们可以在函数中用如下形式定义一个局部寄存器变量:register int res __asm__(“ax”);这里ax是变量res所希望使用的寄存器。5、内联函数在程序中,通过把一个函数声明为内联(inline)函数,就可以让gcc把函数的代码集成到调用该函数的代码中去。这样处理可以去掉函数调用时进入/退出时间开销,从而肯定能够加快执行速度。因此把一个函数声明为内联函数的主要目的就是能够尽量快速的执行函数体。内联函数嵌入调用者代码中的操作是一种优化操作,因此只有进行优化编译时才会执行代码嵌入处理。若编译过程中没有使用优化选项”- 0”,那么内联函数的代码就不会被真正的嵌入到调用者代码中,而是只作为普通函数调用来处理。把一个函数声明为内联函数的方法是在函数声明中使用关键词’inline’,例如内核文件fs/inode.c中的如下函数:函数中的某些语句用法可能会使得内联函数的替换操作无法正常进行,或者不适合进行替换操作。例如使用了可变参数、内存分配函数malloca()、可变长度数据类型变量、非局部goto语句、以及递归函数。编译时可以使用选项-Winline让gcc对标志成inline但不能被替换的函数给出警告信息以及不能替换的原因。ISO标准C99的内联函数语义定义等同于使用组合关键词inline和static的定义,即省略了关键词”static”。若在程序中需要使用C99标准的语义,那么就需要使用编译选项-std=gnu99。不过为了兼容起见,在这种情况下还是最好使用inline和static组合。关键词inline和extern组合在一起的作用几乎类同一个宏定义。使用这种组合方式就是把带有组合关键词的一个函数定义放在.h头文件中,并且把不含关键词的另一个相同函数定义放在一个库文件中。此时头文件中的定义会让绝大数对该函数的调用被替换嵌入。如果还有没有被替换的对该函数的调用,那么就会使用程序文件中或库中的拷贝。Linux 0.1.x内核源代码中文件include/string.h、lib/strings.c就是这种使用方式的一个例子。例如,string.h中定义了如下函数:而在内核函数库目录中,lib/strings.c文件把关键词inline和extern都定义为空,如下所示。因此实际上就在内核函数库中又包含了string.h文件所有这类函数的一个拷贝,即又对这些函数重新定义了一次,并且”消除”了两个关键词的作用。此时库函数中重新定义的上述strcpy()函数变成如下形式:
上节介绍的as86汇编器仅用于编译内核中的boot/bootsect.s引导扇区程序和实模式下的设置程序boot/setup.s。内核中其余所有汇编语言程序(包括C语言产生的汇编程序)均使用as来编译,并与C语言程序编译产生的模块进行链接。实际上as汇编器最初是专门用于汇编gcc产生的中间汇编语言程序的,而非作为一个独立的汇编器使用。因此,as汇编器也支持很多C语言的特性,这包括字符、数字和常数表示方法以及表达式形式等方面。1、编译as汇编语言程序使用as汇编器编译一个as汇编语言程序的基本命令行格式如下所示:as [选项] [-o objfile] [srcfile.s …]其中objfile是as编译输出的目标文件名,srcfile.s是as的输入汇编语言程序名。如果没有指定输出文件名,那么as会编译输出名称为a.out的默认目标文件。一个程序的源代码可以被放置在一个或多个文件中,程序的源代码是如何分割放置在几个文件中并不会改变程序的语义。程序的源代码是所有这些文件按次序的组合结果。每次运行as编译器,它只编译一个源程序,但一个源程序可以由多个文本文件组成(终端的标准输入也是一个文件)。我们可以在as命令行上给出零个或多个输入文件名。as将会按从左到右的顺序读取这些输入文件的内容。在命令行上任何位置处的参数若没有特定含义的话,将会被作为一个输入文件名看待。as的输出文件是输入的汇编语言程序编译生成的二进制数据文件,即目标文件。该目标文件主要用于作为链接器ld的输入文件。2、as汇编语法为了维持与gcc输出汇编程序的兼容性,as汇编器使用AT&T系统V的汇编语法(下面简称为AT&T语法)。这种语法与Intel汇编程序使用的语法(简称Intel语法)很不一样,它们之间的主要区别有以下几点:a、AT&T语法中立即操作数前面要加一个字符’$’;寄存器操作数名前要加一个字符百分号’%’;绝对跳转/调用(相对于与程序计数器有关的跳转/调用)操作数前面要加星号’*’。而Intel汇编语法均没有这些限制;b、AT&T语法与Intel语法使用的源和目的操作数次序正好相反。AT&T的源和目的操作数是从左到右 ‘源, 目的’。例如Intel的语句’add eax, 4’,对应AT&T的语句是’addl $4, %eax’;c、AT&T语法中内存操作数的长度(宽度)由操作码最后一个字符来确定。操作码后缀’b’、’w’、’l’分别指示内存引用宽度为8位字节(byte)、16位字(word)、32位长字(long)。Intel语法则通过在内存操作数前使用前缀’byte ptr’、’word ptr’、’dword ptr’来达到同样的目的。因此,Intel的语句’mov al,byte ptr foo’对应于AT&T的语句’movb $foo,%al’;d、AT&T语法中立即形式的远跳转和远调用为’ljmp/lcall $section,$offset’,而Intel的是’jmp/call far section:offset’。同样,AT&T语法中远返回指令’lret $stack-adjust’对应Intel的’ret far stack-adjust’;e、AT&T汇编器不提供对多代码段程序的支持,UNIX类操作系统要求所有代码在一个段中。(1)、汇编程序预处理as汇编器具有对汇编语言程序内置的简单预处理功能。该预处理功能会调整并删除多余的空格字符和制表符;删除所有注释语句并且使用单个空格或一些换行符替换他们;把字符常数转换成对应的数值。但是该预处理功能不会对宏定义进行处理,也没有处理包含文件的功能。如果需要这方面的功能,那么可以让汇编语言程序使用大写的后缀’.S’,让as使用gcc的CPP预处理功能。as汇编语言程序的注释方法除了使用C语言注释语句(即’/*’和’*/’)以外,还使用井号’#’作为单行注释开始字符。(2)、符号、语句和常数符号(Symbol)是由字符组成的标识符,组成符号的有效字符取自于大小写字符集、数字和三个字符‘_.$’。符号不允许使用数字字符开始,并且大小写含义不同。语句(Statement)以换行符或者行分隔符(’;’)作为结束。文件最后语句必须以换行符作为结束。语句由零个或多个标号(Label)开始,后面可以跟随一个确定语句类型的关键符号。标号由符号后面跟随一个冒号(’:’)构成。如果关键符号以一个’.’开始,那么当前语句就是一个汇编命令(或称为伪指令、指示符)。如果关键符号以一个字母开始,那么当前语句就是一条汇编语言指令语句。常数是一个数字,可以分为字符常数和数字常数两类。字符常数还可分为字符串和单个字符;而数字常数可分为整数、大数和浮点数。字符串必须用双引号括住,并且其中可以使用反斜杠’\’来转义包含特殊字符。例如,’\\’表示一个反斜杠字符。汇编程序中使用单个字符常数时,可以写成在该字符前加一个单引号,例如”’A”表示值65,”’C”表示值67。表3-1中的转义码也同样可以用于单个字符常数。例如“’\\”表示一个普通反斜杠字符常数。整数数字常数有4种表示方法,即使用’0b’或’0B’开始的二进制数(’0-1’);以’0’开始的八进制数(’0-7’);以非’0’数字开始的十进制数(’0-9’)和使用’0x’或’0X’开头的十六进制数(’0-9a-FA-F’)。若要表示负数,只需在前面添加负号’-’。大数(Bignum)是位数超过32位二进制位的数。其表示方法与整数相同。3、指令语句、操作数和寻址指令(Instructions)是CPU执行的操作,通常指令也称为操作码(Opcode);操作数(Operand)是指令操作的对象;而地址(Address)是指定数据在内存中的位置。指令语句是程序运行时刻执行的一条语句,它通常可包含4个组成部分:标号(可选)、操作码(指令助记符)、操作数(由具体指令指定)、注释。一条指令语句可以含有0个或最多3个用逗号分开的操作数。对于具有两个操作数的指令语句,第1个是源操作数,第2个是目的操作数,即指令操作结果保存在第2个操作数中。操作数可以是立即数(即值是常数值的表达式)、寄存器(值在CPU的寄存器中)或内存(值在内存中)。一个间接操作数(Indirect operand)含有实际操作数值的地址值。AT&T语法通过在操作数前加一个’*’字符来指定一个间接操作数。只有跳转/调用指令才能使用间接操作数。立即操作数前需要加一个’$’字符前缀,寄存器名前需要加一个’%’字符前缀。(1)、指令操作码的命名AT&T语法中指令操作码名称(即指令助记符)最后一个字符用了指明操作数的宽度。字符’b’、’w’和’l’分别指定byte、word和long类型的操作数。如果指令名称没有带这样的字符后缀,并且指令语句中不含内存操作数,那么as就会根据目的寄存器操作数来尝试确定操作数的宽度。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语法中转换指令的对应关系如下表所示:(2)、指令操作码前缀操作码前缀用于修饰随后的操作码。它们用于重复字符串指令、提供区覆盖、执行总线锁定操作、或指定操作数和地址宽度。通常操作码前缀可作为一条没有操作数的指令独占一行并且必须直接位于所影响指令之前,但是最好与它修饰的指令放在同一行。操作码前缀及说明如下表所示:(3)、内存引用Intel语法的间接内存引用形式:section:[base + index*scale + disp]。对应的AT&T语法形式:section:disp(base, index, scale)。其中base和index是可选的32位基址寄存器和索引寄存器,disp是可选的偏移值。scale是比例因子,取值范围是1、2、4和8。scale乘上索引index用来计算操作数地址。如果没有指定scale,则scale取默认值1。section为内存操作数指定可选的段寄存器,并且会覆盖操作数使用的当前默认段寄存器。请注意,如果指定的段覆盖寄存器与默认操作的段寄存器相同,则as就不会为汇编的指令再输出相同的段前缀。以下是几个AT&T和Intel语法形式的内存引用例子:(4)、跳转指令跳转指令可分为无条件跳转和条件跳转两大类。条件跳转指令将依赖于执行指令时标志寄存器中某个相关标志的状态来确定是否进行跳转,而无条件跳转指令则不依赖于这些标志。jmp是无条件跳转指令,并可分为直接(direct)跳转和间接(indirect)跳转两类,而条件跳转指令只有直接跳转形式。对于直接跳转指令,跳转到的目标指令的地址是作为跳转指令的一部分直接编码进跳转指令中;对于间接跳转指令,跳转的目的位置取自于某个寄存器或者某个内存位置中。直接跳转语句的写法是给出跳转目标处的标号;间接跳转语句的写法是必须使用一个星字符’*’作为操作指示符的前缀字符,并且该操作指示符使用movl指令相同的语法。下面是直接跳转和间接跳转的例子:jmp NewLoc #直接跳转,无条件直接跳转到标号NewLoc处继续执行jmp *%eax #间接跳转,寄存器%eax的值是跳转的目标位置jmp *(%eax) #间接跳转,从%eax指明的地址处读取跳转的目标位置同样,与指令计数器PC(程序指令计数器:Program Counter)无关的间接调用的操作数也必须有一个’*’作为前缀字符。若没有使用’*’字符,那么as汇编器就会选择与指令计数器PC相关的跳转标号。还有,其他任何具有内存操作数的指令都必须使用操作码后缀(’b’、’w’、’l’)指明操作数的大小(byte、word或long)。4、区与重定位链接器ld会把输入的目标文件中的内容按照一定规律组合生成一个可执行程序。当as汇编器输出一个目标文件时,该目标文件中的代码被默认设置成从从地址0开始。此后ld将会在链接过程中为不同目标文件中的各个部分分配不同最终地址位置。ld会把程序中的字节块移动到程序运行时的地址处。这些块是作为固定单元进行移动的,它们的长度以及字节次序都不会改变。这样的固定单元被称作是区(或段、部分)。而为区分配运行时刻的地址操作就被称为重定位(Relocation)操作,其中包括调整目标文件中记录的地址,从而让它们对应到恰当的运行时刻地址上。as汇编器输出产生的目标文件中至少具有3个区,分别被称为正文(text)、数据(data)和bss区。每个区都可能是空的。如果没有使用汇编命令把输出放置在’.text’或’.data’区中,这些区仍然存在,但内容是空的。在一个目标文件中,text区从地址0开始,随后是data区,再后面是bss区。as使用的所有地址都可表示为:(区) + (区中偏移)。在下面说明中,我们使用记号”{secname N}”来表示区secname中偏移N。除了text、data、bss区,我们还需要了解绝对地址区(absolute区)。当链接器把各个目标文件组合在一起时,absolute区的地址始终不变。目标文件中的absolute区必会重叠而覆盖。另外还有一种名为”未定义的”区(Undefined section)。链接器ld会把程序所有目标文件中的text区放在相邻的地址处。我们习惯上所说的程序的text区实际上是指其所有目标文件text区组合构成的整个地址区域。对程序中data和bss区的理解也是同样如此。(1)、链接器涉及的区链接器ld只涉及如下4类区:text区、data区:这两个区用于保存程序。当程序运行时,则通常text区是不会改变的。当程序运行时,data区的内容通常是会变化的,例如,C变量一般就存放在data区中。bss区:在程序开始运行时,这个区中含有0值字节。absolute区:该区的地址0总是”重定位”到运行时刻地址0处。undefined区:对不在先前所述各个区中对象的地址引用都属于本区。图3-2是3个理想化的可重定位区的例子,其中水平轴表示内存地址:(2)、子区汇编取得的字节数据通常位于text或data区中。有时候在汇编程序某个区中可能分布着一些不相邻的数据组,但是你可能想让它们在汇编后聚集在一起存放。as汇编器允许你利用子区(subsection)来达到这个目的。在每个区中,可以有编号为0-8192的子区存在。编制在同一个子区中的对象会在目标文件中与该子区中的其他对象放在一起。例如,编译器可能想把常数存放在text区中,但是不想让这些常数散布在被汇编的整个程序中。在这种情况下,编译器就可以在每个会输出的代码区之前使用’.text 0’子区,并且在每组会输出的常数之前使用’.text 1’子区。使用子区是可选的,如果没有使用子区,那么所有对象都会被放在子区0中。子区会以其从小到大的编号顺序出现在目标文件中,但是目标文件中并不包含表示子区的任何信息。处理目标文件的ld及其他程序并不会看到子区的踪迹。如果只指定了’.text’,那么就会默认使用’.text 0’;同样的,’.data’表示使用’.data 0’。每个区都有一个位置计数器(Location Counter),它会对每个汇编进该区的字节进行计数。由于子区仅供as汇编器使用方便而设置的,因此并不存在子区计数器。(3)、bss区bss区用于存储局部公共变量。你可以在bss区中分配空间,但是在程序运行之前不能在其中放置数据,因为当程序刚开始执行时,bss区中所有字节内容都将被清零。’.lcomm’汇编命令用于在bss区中定义一个符号;’.comm’可用于在bss区中声明一个公共符号。5、符号在程序编译和链接过程中,符号(Symbol)是一个比较重要的概念。程序员使用符号来命名对象,链接器使用符号来进行链接操作,而调试器使用符号来进行调试。标号(Label)是后面紧跟随一个冒号的符号。符号名以一个字母或’._’字符之一开始。局部符号用于协助编译器和程序员临时使用的名称。在一个程序中共有10个局部符号名(’0’…’9’)可供重复使用。为了定义一个局部符号,只要写出形如’N:’的标号(其中N代表任何数字)。若是引用前面最近定义的这个符号,需要写成’Nb’;若需要引用下一个定义的局部符号,则需要写成’Nf’。其中’b’意思是向后(backwards),’f’表示向前(forwards)。局部标号在使用方面没有限制,但是在任何时候,我们只能向前/向后引用最远10个局部标号。(1)、特殊点符号特殊符号’.’表示as汇编的当前地址。因此表达式’mylab: . long .’就会把mylab定义为包含它自己所处的地址值。给’.’赋值就如同汇编命令’.org’的作用。因此表达式’.=.+4’与’.space 4’完全相同。(2)、符号属性除了名字以外,每个符号都有”值”和”类型”属性。根据输出的格式不同,符号也可以具有辅助属性。如果不定义就使用一个符号,as就会假设其所有属性均为0,这表示该符号是一个外部定义的符号。符号的值通常是32位的。对于标出text、data、bss和absolute区中一个位置的符号,其值是从区开始到标号处的地址值。对于text、data和bss区,一个符号的值通常会在链接过程中由于ld改变区的基地址而变化;absolute区中符号的值不会改变,这也是为何称它们是绝对符号的原因。ld会对未定义符号的值进行特殊处理。如果未定义的符号的值是0,则表示该符号在本汇编程序中没有定义,ld会尝试根据其他链接的文件来确定它的值。在程序中使用了一个符号但没有对符号进行定义,就会产生这样的符号。若未定义符号的值不为0,那么该符号值就表示是.comm公共声明的需要保留的公共存储空间字节长度,符号指向该存储空间的第一个地址处。符号的类型属性含有用于链接器和调试器的重定位信息、指示符号是外部的标志以及一些其他可选信息。对于a.out格式的目标文件,符号的类型属性存放在一个8位字段中(n_type字节),其含义请参见有关include/a.out.h文件的说明。6、汇编命令汇编命令是指示汇编器操作方式的伪指令。所有汇编命令的名称都以’.’开始,其余是字符,并且大小写无关。下面列出一些常用汇编命令:(1)、.align abs-expr1, abs-expr2, abs-expr3.align是存储对齐汇编命令,用于在当前子区中把位置计数器值设置(增加)到下一个指定存储边界处。第1个绝对值表达式abs-expr1(absolute expression)指定要求的边界对齐值。对于使用a.out格式目标文件的80x86系统,该表达式值是位置计数器值增加后其二进制最右面0值位的个数,即是2的次方值。例如,’.align 3’表示把位置计数器值增加到8的倍数上,如果位置计数器值本身就是8的倍数,那么就无需改变。但是对于使用ELF格式的80x86系统,该表达式值直接就是要求对齐的字节数。例如,’.align 8’就是把位置计数器值增加到8的倍数上。第2个表达式给出用于对齐而填充的字节值。该表达式与其前面的逗号可以省略。若省略,则填充字节值是0。第3个可选表达式abs-expr3用于指示对齐操作允许填充跳过的最大字节数。如果对齐操作要求跳过的字节数大于这个最大值,那么该对齐操作就被取消。若想省略第2个参数,可以在第1和第3个参数之间使用两个逗号。(2)、.ascii "string"...从位置计数器所指当前位置为字符串分配空间并存储字符串。可使用逗号分开写出多个字符串。例如,’.ascii “Hello world!”, “My assembler”’。该汇编命令会让as把这些字符串汇编在连续的地址位置处,每个字符串后面不会自动添加0(NULL)字节。(3)、.asciz "string"...该汇编命令与’.ascii’类似,但是每个字符串后面会自动添加NULL字符。(4)、.byte expressions该汇编命令定义0个或多个用逗号分开的字节值。每个表达式的值是1字节。(5)、.comm symbol, length在bss区中声明一个命名的公共区域。在ld链接过程中,某个目标文件中的一个公共符号会与其他目标文件中同名的公共符号合并。如果ld没有找到一个符号的定义,而只是一个或多个公共符号,那么ld就会分配指定长度length字节的未初始化内存。length必须是一个绝对值表达式,如果ld找到多个长度不同但同名的公共符号,ld就会分配长度最大的空间。(6)、.data subsection该汇编命令通知as把随后的语句汇编到编号为subsection的data子区中。如果省略编号,则默认使用编号0。编号必须是绝对值表达式。(7)、.desc symbol, abs-expr用绝对表达式的值设置符号symbol的描述符字段n_desc的16位值。仅用于a.out格式的目标文件。参见有关include/a.out.h文件的说明。(8)、.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。(9)、.global symbol(或者.globl symbol)该汇编命令会使得链接器ld能看见符号symbol。如果在我们的目标文件中定义了符号symbol,那么它的值将能被链接过程中的其他目标文件使用。若目标文件中没有定义该符号,那么它的属性将从链接过程中其他目标文件的同名符号中获得。这是通过设置符号symbol类型字段中的外部位N_EXT来做到的。参见有关include/a.out.h文件的说明。(10)、.int expressions该汇编命令在某个区中设置0个或多个整数值(80386系统为4字节,同.long)。每个用逗号分开的表达式的值就是运行时刻的值。例如,’.int 1234, 567, 0x89AB‘。(11)、.lcomm symbol, length为符号symbol指定的局部公共区域保留长度为length字节的空间。所在的区和符号symbol的值是新的局部公共块的值。分配的地址在bss区中,因此在运行时刻这些字节值被清零。由于符号symbol没有被声明为全局的,因此链接器ld看不见。(12)、.long expressions含义与.int相同。(13)、.octa bignums这个汇编命令指定0个或多个用逗号分开的16字节大数(.byte,.word,.long,.quad,.octa分别对应1、2、4、8和16字节数)。(14)、.org new_lc, fill这个汇编命令会把当前区的位置计数器设置为new_lc。new_lc是一个绝对值(表达式),或者是具有相同区作为子区的表达式,也即不能用.org跨越各区。如果new_lc的区不对,那么.org就不会起作用。请注意,位置计数器是基于区的,即以每个区作为计数起点。当位置计数器值增长时,所跳跃过的字节将被填入值fill,该值必须是绝对值。如果省略了逗号和fill,则fill默认为0值。(15)、.quad bignums这个汇编命令指定0个或多个用逗号分开的8字节大数bignum。如果大数放不进8个字节中,则取低8个字节。(16)、.short expressions这个汇编命令指定某个区中0个或多个用逗号分开的2字节数。对于每个表达式,在运行时刻都会产生一个16位的值。(17)、.space size, fill该汇编命令产生size个字节,每个字节填值fill。这个参数均为绝对值,如果省略了逗号和fill,那么fill的默认值就是0。(18)、.string "string"定义一个或多个用逗号分开的字符串。在字符串中可以使用转义字符。每个字符串都自动附加一个NULL字符结尾。例如,‘.string “\n\nStarting”, “other strings”’。(19)、.text subsection通知as把随后的语句汇编进编号为subsection的子区中。如果省略了编号subsection,则使用默认编号值0。(20)、.word expressions对于32位机器,该汇编命令含义与.short相同。7、编写16位代码虽然as通常用来编写纯32位的80x86代码,但是1995年后它对编写运行于实模式或16位保护模式的代码也提供有限的支持。为了让as汇编时产生16位代码,需要在运行于16位模式的指令语句之前添加汇编命令’.code16’,并且使用汇编命令’.code32’让as汇编器切换回32位代码汇编方式。16位模式下,因为as为所有指令添加了额外的地址和操作数宽度前缀,所以汇编产生的代码长度和性能上将会受到影响。8、as汇编器命令行选项-a 开启程序列表-f 快速操作-o 指定输出的目标文件名-R 组合数据区和代码区-W 取消警告信息
这里是这篇文章涉及到的一些代码和工具,各位请自行下载吧。文件下载地址:点此下载 密钥:7juy。在Linux 0.1.x系统中使用了两种汇编器(Assembler)。一种是能产生16位代码的as86汇编器,使用配套的ld86链接器;另一种是GNU的汇编器as,使用GNU ld链接器来链接产生的目标文件。as86和ld86是由MINIX-386的主要开发者之一Bruce Evans编写的Intel 8086、80386汇编编译程序和链接程序。Linux系统仅用它来创建16位的启动引导扇区程序boot/bootsect.s和实模式下初始设置程序boot/setup.s的二进制执行代码。由于Linux系统仅使用as86和ld86编译和链接上面提到的两个16位汇编程序bootsect.s和setup.s,因此这里仅介绍这两个程序中用到的一些汇编程序语法和汇编命令的作用和用途。1、as86汇编语言语法汇编器专门用来把低级汇编语言程序编译成含机器码的二进制程序或目标文件。汇编器会把输入的一个汇编语言程序(例如srcfile)编译成目标文件(objfile)。汇编的命令行基本格式是:as86 [选项] –o objfile srcfile其中选项用来控制编译过程以产生指定格式和设置的目标文件。输入的汇编语言程序srcfile是一个文本文件,该文件内容必须是由换行字符结尾的一系列文本行组成。一条汇编语句通常由标号(可选)、指令助记符(指令名)和操作数三个字段组成。汇编器编译产生的目标文件objfile通常起码包含3个段(section),即正文段(.text)、数据段(.data)和未初始化数据段(.bss)。2、汇编语言程序下面我们以一个简单的框架示例程序boot.s来说明as86汇编程序的结构以及程序中语句的语法,然后给出编译链接和运行方法,最后再分别列出as86和ld86的使用方法和编制选项。示例程序如下所示,这个示例是bootsect.s的一个框架程序,能编译生成引导扇区代码。其中为了演示说明某些语句的使用方法,故意加入了无意义的第20行语句。该程序是一个简单的引导扇区启动程序,编译链接产生的执行程序可以放入软盘的第1个扇区直接用来引导计算机启动。启动后会在屏幕的第17行,第5列处显示出红色字符串”Loading system ...”,并且光标下移一行,然后程序就在第27行上死循环。在as86汇编语言程序中,凡是以感叹号’!’或分号’;’开始的语句其后面均为注释文字。注释语句可以放在任何语句的后面,也可以从一个新行开始。‘.global‘是汇编指示符(或称为汇编伪指令、伪操作符),用于定义随后的标号标识符是外部的或全局的,并且即使不使用也强制引入。汇编指示符均以一个字符’.’开始,并且不会在编译时产生任何代码。汇编指示符由1个伪操作码,后跟0个或多个操作数组成。’global’是一个伪操作码,而其后面的标号’begtext, begdata, begbss’等就是它的操作数。标号是后面带冒号的标识符,例如第6行上的’begtext:’。但在引用一个标号时无须带冒号。第5行到第11行上除定义了3个标号外,还定义了3个伪操作符:’.text’、’.data’、’.bss’。它们分别对应汇编程序编译产生目标文件中的3个段,即正文段、数据段和未初始化数据段。’.text’用于标识正文段的开始位置,并切换到text段;’.data’用于标识数据段的开始位置,并把当前段切换到数据段;’.bss’用于标识未初始化数据段的开始,并把当前段改变成bss段。因此行5-11用于在每个段中定义一个标号,最后再切换到text段开始编写随后的代码。这里把三个段都定义在同一重叠地址范围中,因此本示例程序实际上不分段。第12行定义了一个赋值语句”BOOTSEG = 0x07c0”。等号’=’(或符号’EQU’)用于定义标识符BOOTSEG所代表的值,因此这个标识符可称为符号常量。这个值与C语言中的写法一样,可以使用十进制、八进制和十六进制。标识符’entry’是保留关键字,用于迫使链接器ld86在生成的可执行文件中包括进其后指定的标号’start’。entry start告诉编译器程序入口是start,我们这段是引导代码,引导代码在引导盘的指定位置处,BIOS会读取引导盘的第一个扇区到内存0x7c00:0x00处。注意:程序运行在实模式下面,物理地址=代码段cs*16+偏移,要想在0x7c00处开始执行,要设BOOTSEG=0x07c0,而不是0x7c00。jmpi go,BOOTSEG是跳转到0x7c00处执行,在《2.3主存储器、BIOS和CMOS存储器》的第2小节”基本输入/输出程序BIOS”,我们说到BIOS会加载引导程序到内存的0x7c00处,并且跳转到这个地方继续执行。而jmpi 这条语句使得引导程序从0x7c00开始执行,也就是说,开始执行我们当前的引导区程序。在as86中,寻址方式有以下这些:(1)、直接寄存器寻址跳转到bx值指定的地址处,即把bx的值拷贝到IP中。mov bx,axjmp bx(2)、间接寄存器寻址bx值指定内存位置处的内容作为跳转的地址。mov [bx],axjmp [bx](3)、立即寻址把立即数1234放到ax中,把msg1地址值放到ax中。立即数前一定要加’#’号,否则将作为内存地址使用而使语句变成绝对寻址语句。另外,把一个标号(例如msg1)的地址值放入寄存器中时也一定要在前面加’#’ 号,否则会变成把msg1地址处的内容放到寄存器中。mov ax,#1234mov ax,#msg1(4)、绝对寻址把内存地址1234(msg1)处的内容放入ax中。mov ax,1234mov ax,msg1mov ax,[msg1](5)、索引寻址把第2个操作数所指内存位置处的值放入ax中。mov ax,msg1[bx]mov ax,msg1[bx*4+si]第26行是BIOS屏幕显示中断调用:int 0x10。这里使用其功能19、子功能1。该中断的作用是把一字符串(msg1)写到屏幕指定位置处。寄存器ax的AH存储的是功能号,AL存储的是子功能号。寄存器cx中是字符串长度值,dx中是显示位置值,bx中是显示使用的字符属性,es:bp指向字符串。关于BIOS中断和int 10H的介绍,可以参考我整理的文章:《BIOS 中断向量表》、《BIOS int 10H中断介绍》。第27行是一个跳转语句跳转到当前指令处,因此这是一个死循环语句。这里采用死循环语句是为了让显示的内容能够停留在屏幕上而不被删除。死循环语句是调试汇编程序时常有的方法。第28-29行定义了字符串msg1。定义字符串需要使用伪操作符’.ascii’,并且需要使用双引号括住字符串。伪操作符’.asciiz’还会自动的在字符串后添加一个NULL(0)字符。另外,第29行上定义了回车和换行(13,10)两个字符。定义字符需要使用伪操作符’.byte’,并且需要使用单引号把字符括住,当然也可以像程序中一样直接写出字符的ASCII码。第30行上的伪操作符语句’.org’定义了当前汇编的位置。这条语句会把汇编器编译过程中当前段的位置计数器值调整为该伪操作符语句上给出的值。对于本示例程序,该语句把位置计数器值设置为510,并在此处(第31行)放置了有效引导扇区标志字0XAA55。伪操作符’.word’用于在当前位置定义一个双字节内存对象(变量),其后可以是一个数字或者是一个表达式。由于后面没有代码或数据了,因此我们可以据此确定boot.s编译出来的可执行程序应该正好为512字节。3、as86汇编语言程序的编译和链接对上面的程序编写好之后,接下来就可以编译、链接上面的示例程序,依次执行下面的命令(我是在Ubuntu14.04中进行编译的):# as86 -0 -a -o boot.o boot.s //编译,生成与as部分兼容的目标文件# ld86 -0 -s -o boot boot.o //链接,去掉符号信息# dd bs=32 if=boot of=boot.img skip=1 //写入软盘或Image盘文件中第1条命令利用as86汇编器对boot.s程序进行编译,生成boot.o目标文件。第2条命令使用链接器ld86对目标文件执行链接操作,最后生成可执行文件boot。其中选项’-0’用于生成8086的16位目标程序;’-a’用于指定生成与GNU as和ld部分兼容的代码;’-s’用于告诉链接器要去除最后生成的可执行文件中的符号信息。’-o’指定生成的可执行文件名称。从上面ls命令列出的文件信息可以看出,最后生成的boot程序并不是前面所说的正好512字节,而是多了32字节,这32字节是文件头信息。为了能够使用这个程序引导启动机器,需要人工去掉这32字节。这里我们通过dd命令去除boot中的前32字节,并且新生成的文件名为boot.img。在我提供的文件中,有一个bochs的安装包,大家安装一下吧。接着将得到的boot.img和” bochsrc.bxrc”文件放在同一个目录下面,然后双击” bochsrc.bxrc”运行程序,得到的结果如下图所示:4、as86和ld86使用方法和选项(1)、as86的使用方法和选项as86 [-03agjuw] [-b [bin]] [-lm [list]] [-n name] [-o objfile] [-s sym] 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 不显示警告信息(2)、ld链接器的使用方法和选项ld86 [-03Mimrs[-]] [-T textaddr] [-llib_extension] [-o outfile] infile...默认设置(除了以下默认值以外,其他选项默认为关闭或无):-3 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 在目标文件中删除所有符号
1、中断控制器80x86兼容机使用两片级联的8259A可编程中断控制芯片组成一个中断控制器,用于实现I/O设备的中断控制数据存取方式,并且能为15个设备提供独立的中断控制功能,如下图所示:当一台PC机刚上电时,上图中的硬件中断请求号会被ROM BIOS设置成下表所示的对应中断向量号。Linux操作系统并不直接使用这些PC机默认设置好的中断向量号,当Linux系统执行初始化操作时,它会重新设置中断请求号与中断向量号的对应关系。表2-2 开机时ROM BIOS设置的硬件请求处理中断号中断请求号BIOS设置的中断号用途IRQ00x08(8)8253发出的100HZ时钟中断IRQ10x09(9)键盘中断IRQ20x0A(10)连接从芯片IRQ30x0B(11)串行口2IRQ40x0C(12)串行口1IRQ50x0D(13)并行口2IRQ60x0E(14)软盘驱动器IRQ70x0F(15)并行口1IRQ80x70(112)实时钟中断IRQ90x71(113)改向至INT 0x0AIRQ100x72(114)保留IRQ110x73(115)保留(网络接口)IRQ120x74(116)PS/2鼠标口中断IRQ130x75(117)数学协处理器中断IRQ140x76(118)硬盘中断IRQ150x77(119)保留2、DMA控制器通常DMA控制器由机器上的Intel 8237芯片或其兼容芯片实现。通过对DMA控制器进行编程,外设与内存之间的数据传输能在不受CPU控制的条件下进行,因此在数据传输期间,CPU可以做其他事情。在PC机中,使用了两片8237芯片,因此DMA控制器有8个独立的通道可供使用。其中后4个是16位通道,软盘控制器被专门指定使用DMA通道2。在使用一个DMA通道之前,涉及到对3个端口的设置:页面寄存器端口、(偏移)地址寄存器端口和数据计数寄存器端口。由于DMA寄存器是8位的,而地址和计数值是16位的,因此各自需要发送两次。3、定时/计数器Intel 8253/8254是一个可编程定时/计数器(PIT-Programmable Interval Timer)芯片,用于处理计算机中的精确时间延迟。该芯片提供了3个独立的16位计数器通道,每个通道可工作在不同的工作方式下,并且这些工作方式均可以使用软件来进行设置。在软件中进行延时的一种方法是执行循环操作语句,但这样做很耗CPU时间。若机器中采用了8253/8254芯片,那么程序员就可以配置8253以满足自己的要求,并且使用其中一个计数器通道达到所期望的延时。在延时时间到了后,8253/8254将会向CPU发送一个中断信号。对于PC及其兼容微机系统所采用的是8254芯片。3个定时/计数器通道被分别用于日时钟计时中断信号、动态内存DRAM刷新定时电路和主机扬声器音调合成。Linux 0.11操作系统只对通道0进行了重新设置,使得该计数器工作在方式3下,并且每间隔10ms发出一个信号以产生中断请求信号(IRQ0)。这个间隔定时产生的中断请求就是Linux 0.11内核工作的脉搏,它用于定时切换当前执行的任务和统计每个任务所使用的系统资源量(时间)。4、键盘控制器当一个键被按下时,键盘发送的扫描码称为接通扫描码(Make code),或简称为接通码;当一个被按下的按键放开时发送的扫描码被称为断开扫描码(Break code),或简称为断开码。主机键盘控制器专门用来对接收到的键盘扫描码进行解码,并把解码后的数据发送到操作系统的键盘数据队列中。因为每个按键的接通和断开码是不同的,所以键盘控制器根据扫描码就可以确定用户在操作哪个按键。整个键盘上所有按键的接通和断开码就组成了键盘的一个扫描码集(Scan Code Set)。根据计算机的发展,目前已经有3套扫描码集可供使用,它们分别是:a、第一套扫描码集:原始XT键盘扫描码集。目前的键盘已经很少发送这类扫描码;b、第二套扫描码集:现代键盘默认使用的扫描码集,通常称为AT键盘扫描码集;c、第三套扫描码集:PS/2键盘扫描码集。原IBM推出PS/2微机时使用的扫描码集,已很少使用。AT键盘默认发送的是第二套扫描码集,虽然如此,主机键盘控制器为了与PC/XT机的软件兼容,仍然会把所有接收到的第二套键盘扫描码转换成第一套键盘扫描码,如图2-7所示。因此,我们在为键盘控制器进行编程时通常只需要了解第一套键盘扫描码即可。这也是后面涉及键盘编程内容时,只给出XT键盘扫描码集的原因。键盘控制器通常采用Intel 8042单片微处理器芯片或其兼容电路。现在的PC机都已经将键盘控制器集成在主板芯片组中,但是功能仍然与使用8042芯片的控制器相兼容。键盘控制器接收键盘发送来的11位串行格式数据。其中第1位是起始位,第2-9位是8位键盘扫描码,第10位是奇校验校验位,第11位是停止位。键盘控制器在接收到11位的串行数据后就将键盘扫描码转换成PC/XT标准键盘兼容的系统扫描码,然后通过中断控制器IRQ1引脚向CPU发送中断请求,当CPU响应该中断请求后,就会调用键盘中断处理程序来读取控制器中的XT键盘扫描码。当一个按键被按下时,我们可以从键盘控制器端口接收到一个XT键盘接通码。例如,按下键’A’的接通码是30(0x1E)。当一个按下的键被松开时,从键盘控制器端口收到的就是一个断开码。对于XT键盘,断开码是其接通码加上0x80,即最高有效位(bit7)置位时的接通码。例如,’A’键的断开码就是0x1E + 0x80 = 0x9E。对于那些PC/XT标准83键键盘以后新添加的AT键盘上的按键(例如右边的Ctrl键和右边的Alt键等),则其接通和断开扫描码通常有2到4个字节,并且第1个字节一定是0xE0。例如,按下左边的非扩展Ctrl键时,会产生1字节接通码0x1D;而按下右边的Ctrl键时就会产生扩展的2字节接通码0XE0、0x1D,对应的断开码是0xE0、0x9D。5、串行控制卡串行通信是指在线路上以比特位数据流一次一个比特进行传输的通信方式。串行通信分为同步和异步两种类型,它们之间的主要区别在于传输时同步的通信单位或帧的长度不同。异步串行通信以一个字符作为一个通信单位或一帧进行传输,而同步串行通信则以多个字符或字节组成的序列作为一帧数据进行传输。异步串行通信传输的帧格式如下图2-9所示。传输一个字符由起始位、数据位、奇偶校验位和停止位组成。其中起始位起同步作用,其值恒为0。数据位是传输的实际数据,其长度可以是5~8个比特。奇偶校验位可有可无,由程序设定。停止位恒为1,可由程序设定为1、1.5或2个比特位。在通信开始发送信息之前,双方必须设置成相同的格式,如具有相同数量的数据比特位和停止位。在异步通信规范中,把传送1称为传号(MARK),传送0称为空号(SPACE)。为实现串行通信,PC机上通常都带有2个符合RS-232C标准的串行接口,并使用通用异步接收/发送器控制芯片UART(Universal Asyncronous Receiver/Transmitter)组成的串行控制器来处理串行数据的收发工作。以前的PC机都使用NS8250或NS16450 UART芯片。现在的PC机使用16650A及其兼容芯片,它支持FIFO传输方式,在这种方式下,UART可以在接收或者发送了最多16个字符后才引发一次中断,从而可以减轻系统和CPU的负担。当PC机上电启动时,系统RESET信号通过NS8250的MR引脚是的UART内部寄存器和控制逻辑复位。此后要使用UART就需要对其进行初始化编程操作,来设置UART的波特率、数据位以及工作方式等。6、显示控制对于PC及其兼容机,可以使用彩色和单色显示卡。IBM最早推出的PC机视频系统标准有单色MDA标准,彩色CGA标准以及EGA和VGA标准。以后推出的所有高级显示卡(包括现在的AGP显示卡)虽然都具有极高的图形处理速度和智能加速处理功能,但它们都还是支持这几种标准。Linux 0.1.x操作系统仅使用了这几种标准都支持的文本显示方式。(1)、MDA显示标准单色显示适配器MDA(Monochrome Display Adapter)仅支持黑白两种颜色显示。并且只支持独有的文本字符显示方式(BIOS显示方式为7)。其屏幕显示规格为80列x25行(列号x=0…79,行号y=0…24),共可显示2000个字符。每个字符还带有一个属性字节,因此显示一屏(一帧)内容需要占4KB字节(2000*2=4000B)。其中偶地址字节存放字符代码,奇地址字节存放显示属性。MDA卡配置有8KB显示内存。在PC机内存寻址范围中,占用从0xB0000开始的8KB空间(0xB0000---0xB2000)。如果显示屏行数是video_num_lines=25,列数是video_num_colums=80,那么位于屏幕列行值x、y处的字符和属性在内存中的位置是:字符字节位置 = 0xB0000 + video_num_colums*2*y + x*2;属性字节位置 = 字符字节位置 + 1;在MDA单色文本显示方式中,每个字符的属性字节格式如表2-4所示。其中,D7置1会使字符闪烁,D3置1会使字符高亮度显示。它与图2-10中的彩色文本字符的属性字节基本一致,但只有两种颜色:白色(0x111)和黑色(0x000)。(2)、CGA显示标准彩色图形适配器CGA(Color Graphics Adapter)支持7种彩色和图形显示方式(BIOS显示方式0---6)。在80列x25行的文本字符显示方式下,有单色和16色彩色两种显示方式(BIOS显示方式2---3)。CGA卡标配有16KB显示内存(占用内存地址范围:0xB8000---0xBC000),因此其中共可以存放4帧显示信息。同样,在每一帧4KB显示内存中,偶地址字节存放字符代码,奇地址字节存放显示属性。但在console.c程序中只使用了其中8KB显示内存(0xB8000 --- 0xBA000)。在CGA彩色文本显示方式中,每个显示字符的属性字节格式定义如图2-10所示:与单色显示一样,图中D7置1用于让显示字符闪烁,D3置1用于让字符高亮度显示,比特位D6、D5、D4和D2、D1、D0可以分别组合出8种颜色。前景色与高亮度比特位组合可以显示另外8种字符颜色。这些组合的颜色见表2-5所示:(3)、EGA/VGA显示标准增强型图形适配器EGA(Enhanced Graphics Adapter)和视频图形阵列VGA(Video Graphics Adapter)除兼容或支持MDA和CGA的显示方式以外,还支持其他在图形显示方面的增强显示方式。在与MDA和CGA兼容的显示方式下,占用的内存地址起始位置和范围都分别相同。但EGA/VGA都标配有起码32KB的显示内存。在图形方式下占用从0xA0000开始的物理内存地址空间。7、软盘和硬盘控制器在盘片上存储数据的基本方式是利用盘片表面上的一层磁性介质在磁化后的剩磁状态。软盘通常使用聚酯薄膜作基片,而硬盘片则通常使用金属铝合金作基片。一张软盘中含有一张聚酯薄膜圆盘片,使用上下两个磁头在盘片两面读写数据,盘片旋转速率大约300转/分钟。硬盘中通常起码包括2张或更多张金属盘片,因此具有多个读写磁头。例如,对于包含2个盘片的硬盘中就具有4个物理磁头,含有4个盘片的磁盘中有8个读写磁头。如图2-11所示。磁盘旋转速率很快,通常在4500转/分钟到10000转/分钟,因此磁盘数据的传输速度通常可以达到几十兆比特/秒。为了读写磁盘(软盘和硬盘)上的数据,就必须使用磁盘控制器。磁盘控制器是CPU和驱动器之间的逻辑接口电路,它从CPU接收请求命令,向驱动器发送寻道、读/写和控制信号,并且控制和转换数据流形式。PC机中软盘驱动控制器FDC(Floopy Disk Controller)采用的是NEC μPD765或其兼容芯片。它主要用于接收CPU发出的命令,并根据命令要求向驱动器输出各种硬件控制信号,见图2-13所示。在执行读/写操作时,它需要完成数据的转换(串---并)、编码和校验操作,并且时刻监视驱动器的运行状态。对磁盘控制器的编程过程就是通过I/O端口设置控制器中的相关寄存器内容,并通过寄存器获取操作的结果信息。至于扇区数据的传输,则软盘控制器与硬盘控制器不同。软盘控制器电路采用DMA信号,因此需要使用DMA控制器实施数据传输。而硬盘控制器采用高速数据块进行传输,不需要DMA控制器的介入。由于软盘片比较容易遭到损坏(发霉或划伤),因此目前计算机中已经逐渐开始不配置软盘驱动器,取而代之的是使用容量较大并且更容易携带的U盘存储器。
1、主存储器首先声明一下,这里的主存储器指的是内存。最开始的计算机支持的内存很小,随着计算机技术的发展,计算机所配置的内存越来越大。为了与原来的PC机在软件上兼容,系统1MB以下物理内存使用分配上仍然保持与原来的PC机基本一致,只是原来系统ROM中的基本输入输出程序(BIOS)一直处于CPU能寻址的内存最高端位置处,而BIOS原来所在的位置将在计算机开机初始化时被用作BIOS的影子(Shadow)区域,即BIOS代码仍然会被复制到这个区域中。PC机使用4G内存时,内存的使用区域如下图所示:当计算机上电初始化时,物理内存被设置成从地址0开始的连续区域。除了地址从0xA0000到0xFFFFF(640K到1M共384K)和0xFFFE0000到0xFFFFFFFF(4G处的最后一64K)范围以外的所有内存都可用作系统内存。这两个特定范围被用于I/O设备和BIOS程序。假如我们的计算机有16MB的物理内存,那么在Linux 0.1.x系统中,0-640K将被用作存放内核代码和数据。Linux内核不使用BIOS功能,也不使用BIOS设置的中断向量表。640K-1M之间的384K仍然保留用于图中指明的用途。其中地址0xA0000开始的128K用作显示内存缓冲区,随后部分用于其他控制卡的ROM BIOS或其他映射区域,而0xF0000到1M范围用于高端系统ROM BIOS的映射区。1M-16M将被内核用作可分配的主内存区。另外高速缓冲区和内存虚拟盘也会占用内核代码和数据后面的一部分内存区域,该区域通常会跨越640K-1M的区域。2、基本输入/输出程序 BIOS存放在ROM中的系统BIOS程序主要用于计算机开机时执行系统各部分的自检,建立起操作系统需要使用的各种配置表,例如中断向量表、磁盘参数表。并且把处理器和系统其余部分初始化到一个已知状态,而且还为DOS等操作系统提供硬件设备接口服务。但是由于BIOS提供的这些服务不具备可重入性(即其中程序不可并发运行),并且从访问效率方面考虑,Linux操作系统只在初始化时会利用BIOS提供一些系统参数,在Linux运行时并不使用BIOS中的功能。当计算机系统上电开机或者按了机箱上的复位按钮时,CPU会自动把代码段寄存器CS设置为0xF000,其段基地址则被设置为0xFFFF0000,段长度设置为64KB。而IP被设置为0xFFF0,因此此时CPU代码指针指向0XFFFFFFF0,即4G空间最后一个64K的最后16字节处(这里貌似计算出的CPU地址是段基地址和IP相加得出的结果,应该是遵循80x86保护模式下,地址的计算方法,不知是否正确,希望各位指教,后续第4章节也会详细讲述80x86保护模式及其编程)。由上图可知,这里正是系统ROM BIOS存放的位置。并且BIOS会在这里存放一条跳转指令JMP跳转到BIOS代码中64KB范围内的某一条指令开始执行。由于目前PC机BIOS容量大多有1MB到2MB,并存储在ROM中,因此为了能够执行或访问BIOS中超过64KB范围并且又远远不在0-1M地址空间中的其他BIOS代码或数据,BIOS程序会首先使用一种称为32位大模式(Big Mode)技术把数据段寄存器的访问范围设置成4G(而非原来的64K),这样就可以在0到4G范围内执行和操作数据。此后,BIOS在执行了一系列硬件检测和初始化操作之后,就会把与原来PC机兼容的64KB BIOS代码和数据复制到内存低端1M末端的64K处,然后跳转到这个地方并且让CPU进入真正的实地址模式工作,最后BIOS就会从硬盘或其他块设备把操作系统引导程序加载到内存0x7c00处,并跳转到这个地方继续执行引导程序,图示如下:3、CMOS存储器在PC机中,除了需要使用内存和ROM BIOS以外,还需要使用只有很少存储容量的(只有64或128字节)CMOS(Complementary Metal Oxide Semiconductor,互补金属氧化物半导体)存储器来存放计算机的实时时钟信息和系统硬件配置信息。这部分内存通常和实时时钟芯片(Real Time Chip)做在一块集成块中。CMOS内存的地址空间在基本内存地址空间之外,需要使用I/O指令来访问。
1、I/O端口和寻址CPU为了访问I/O接口控制器或控制卡上的数据和状态信息,需要首先指定它们的地址。这种地址就称为I/O端口地址或者简称端口。通常一个I/O控制器包含访问数据的数据端口、输出命令的命令端口和访问控制器执行状态的状态端口。端口地址的设置方法一般有两种:统一编址和独立编址。端口统一编址的原理是把I/O控制器中的端口地址归入存储器寻址地址空间范围内。因此这种编址方式也称为存储器映像编址。CPU访问一个端口的操作与访问内存的操作一样,也使用访问内存的指令。端口独立编址的方法是把I/O控制器和控制卡的寻址空间单独作为一个独立的地址空间对待,称为I/O地址空间。每个端口有一个I/O地址与之对应,并且使用专门的I/O指令来访问端口。IBM PC及其兼容微机主要使用独立编址方式,采用了一个独立的I/O地址空间对控制设备中的寄存器进行寻址和访问。使用ISA总线结构的传统PC机其I/O地址空间范围是0x000~0x3ff,有1024个I/O端口地址可供使用。各个控制器和控制卡所默认分配使用的端口地址范围如下表所示:表2-1 I/O端口地址分配端口地址范围分配说明0x000 --- 0x01F8237A DMA控制器10x020 --- 0x03F8259A可编程中断控制器10x040 --- 0x05F8253/8254A 定时计数器0x060 --- 0x06F8042键盘控制器0x070 --- 0x07F访问CMOS RAM/实时时钟RTC(Real Time Clock)端口0x080 --- 0x09FDMA页面寄存器访问端口0x0A0 --- 0x0BF8259A可编程中断控制器20x0C0 --- 0x0DF8237A DMA控制器20x0F0 --- 0x0FF协处理器访问端口0x170 --- 0x177IDE硬盘控制器10x1F0 --- 0x1F7IDE硬盘控制器00x278 --- 0x27F并行打印机端口20x2F8 --- 0x2FF串行控制器20x378 --- 0x37F并行打印机端口10x3B0 --- 0x3BF单色MDA显示控制器0x3C0 --- 0x3CF彩色CGA显示控制器0x3D0 --- 0x3DF彩色EGA/VGA显示控制器0x3F0 --- 0x3F7软盘控制器0x3F8 --- 0x3FF串行控制器1另外,IBM PC机也部分的使用了统一编址方式。例如,CGA显示卡上显示内存的地址就直接占用了存储器地址空间0xB800 --- 0xBC00范围。因此若要让一个字符显示在屏幕上,可以直接使用内存操作指令往这个内存区域执行写操作。对于使用EISA或者是PCI等总线结构的现代PC机,有64KB的I/O地址空间可供使用。在普通Linux系统下通过查看/proc/ioports文件可以得到相关控制器或设置使用的I/O地址范围,如下如所示:2、接口访问控制PC机I/O接口数据传输控制方式一般可采用:程序循环查询方式、中断处理方式和DMA传输方式。循环查询方式是指CPU通过在程序中循环查询指定设备控制器中的状态来判断是否可以与设备进行数据交互。这种方式不需要过多的硬件支持,使用和编程都比较简单,但是特别耗费CPU宝贵的时间。因此在多任务操作系统中,除非等待时间极短或必须,否则就不应该使用这种方式。在Linux操作系统中,只有在设备或控制器能够立刻返回信息时,才会在很少的几个地方采用这种方式。中断处理控制方式需要中断控制器的支持。在这种控制方式下,只有当I/O设备通过中断向CPU提出处理请求时,CPU才会暂时中断当前执行的程序转而去执行相应的I/O中断处理服务程序。当执行完该中断处理服务程序后,CPU又会继续执行刚才被中断的程序。在I/O控制器或设备发出中断请求时,CPU通过使用中断向量表(或中断描述表)来寻址相对应的中断处理服务程序的入口地址。因此采用中断控制方式时,要首先设置好中断向量表,并编制好相应的中断处理服务程序。Linux操作系统中,大多数设备I/O控制都采用中断处理方式。直接存储器访问DMA(Direct Memory Access)方式用于I/O设备与系统内存间进行批量数据传送,整个操作过程需要使用专门的DMA控制器来进行而无需CPU插手。由于在传输过程中不需要软件介入,因此操作效率很高。在Linux操作系统中,软盘驱动程序使用中断和DMA方式配合来实现数据的传输工作。
- 1
本站信息
目前本站共被浏览 165577 次
目前本站已经运行 3577 天
目前本站共有 165 篇文章
目前本站共有 6 条评论信息
目前本站共有 104 个标签
目前本站共有 0 条留言信息
网站创建时间: 2015年03月01日
最近更新时间: 2023年11月26日
目前本站已经运行 3577 天
目前本站共有 165 篇文章
目前本站共有 6 条评论信息
目前本站共有 104 个标签
目前本站共有 0 条留言信息
网站创建时间: 2015年03月01日
最近更新时间: 2023年11月26日
JLink V9掉固件修复(灯不亮) 3Zephyr笔记2 - 在STM32F429上运行HelloWorld 2计算NandFlash要传入的行地址和列地址 1Linux MMC子系统 - 6.eMMC 5.1工作模式-设备识别模式 0Linux MMC子系统 - 5.eMMC 5.1工作模式-引导模式 0Linux MMC子系统 - 4.eMMC 5.1常用命令说明(2) 0
最新评论
标签云
Linux嵌入式实用技巧ARM内核学习问题集合CC++编程语言阅读笔记汇编Linux内核完全注释Windows驱动开发计算机基础ARM11ARMv7-ASTM32IDESublimeLinux内核学习eMMCMMC子系统Ubuntu操作系统OfficeVMWareAPUEgccRTOS中断漫游世界随笔感悟开发工具软件应用编程VsCodearmccarmclang编译器ZephyrSPIJLink网卡驱动安装各种芯片库函数NFSμCOS内核sambaFlashUnix命令与脚本输入法Linux内核设计与实现gitRIFFWAVJATGFTPar8161安装centos有线上网μCGUI字库工程建立右键菜单网络文件系统Firefox百度NTFS文件系统CodeBlocksCentOS数据结构算法PhotoShop51KeilQTUltraEditscanfglibc宏定义UIDGID优先级娱乐天地SourceInsight磁盘扇区总线I2CPDFBComparePythonI2SFPUMakefileSWDCPUARP软件推荐FileZilla