第三章 内核编程语言和环境
这章知识有点多,略为繁杂,所以本人打算看一部分记一部分。
这章主要是介绍Linux系统中如何将代码编译产生二进制机器码,即汇编语言和汇编器的相关知识。
as汇编器
Linux 0.1x系统使用了两种汇编器,一个是as86汇编器,使用ld86链接器,另一个是GNU的gas汇编器,用GNU ld链接。as86汇编器主要编译创建16位的启动引导扇区程序boot/bootsect.s 和实模式下的初始设置程序boot/setup.s 的二进制执行代码。GNU的as编译器则负责其它所有代码的编译生成。
as86汇编语法注意
编译器在编译时都有生成的目标文件obj和源代码文件src,编译器产生的目标文件中至少包含以下三个区,.text正文,.data数据和.bss未初始化段,正文和数据都已经初始化过,正文存储的是代码和只读数据,数据段存放可读写数据,未初始化的.bss段是未初始化的,通常目标文件不会为其留出空间。在链接时会被全部初始化为0.
as86汇编器对于boot.s程序编译时生成boot.o目标文件,再用ld86进行链接操作,生成MINIX结构的可执行文件,但在Linux中用作引导时需要去除其MINIX文件头结构。as86和ld86详细的使用方法不做具体介绍。
GNU as汇编器
GNU的as汇编器用于系统中除as86编译的程序编译,并可以与C语言程序编译产生的模块链接。as编译器的源程序可以是多个,但都作为一个文件来读,相当于只有一个输入,目标文件生成为.o文件,与as86类似。下面详细介绍as 汇编语言用法。
as汇编语言语法和intel的汇编语言有很大不同,被称为AT&T语法,最主要的区别是以下几点:
- AT&T语法立即操作数前面加”$”,而寄存器操作数前面要加%与程序计数器相关的语句操作数前面要加*,intel没有限制。
- AT&T语法的源操作数和目的操作数与intel的位置正好相反,他是源在前,目的在后。
- AT&T语法中内存操作数的长度由操作码的最后一个字符决定比如b for byte, w for word, l for long,Intel则通过前缀区分比如Intel的是mov al,byte ptr foo,而AT&T中是movb foo,%al 。
- AT&T不提供对多代码段的支持,UNIX类操作系统要求所有代码在一个段中。
编译时as汇编器对于程序有一个简单的预处理,删除多余空格,制表位等,删除注释语句。
指令,操作数,寻址
指令最多包括 (标号): 操作码 操作数 注释,最多3个操作数
操作数有立即数(值是常数的表达式)(需要加),寄存器(值在CPU的寄存器中)(需要加%)或内存(值在内存中)
操作码的名称AT&T和intel的基本相同。
寻址时,intel形式:section :[base +index *scale +disp]
AT&T形式: section : disp (base, index, scale )
跳转指令分为直接和间接跳转,直接没什么好说的,间接是要加一个前缀,for example jmp %eax 跳转到eax寄存器的值的位置
区与重定位
区是汇编器和链接器在产生目标文件时,一块会被系统同等对待的,以固定单元移动,分配的程序块。
重定位就是将区分配运行时刻的地址的操作。
as汇编器产生目标文件至少有3个区,text区,data区,bss区,text是从地址0开始,然后是data,然后是bss。绝对地址区(absolute区)是在各个目标文件组合在一起时地址不会改变的区,多个目标文件链接absolute区的内容一定会被覆盖重叠。
链接器对于区的处理在后面有详细讲解。
as汇编命令(伪指令)
不作详细解释,共20个,参考书45页。
C语言程序
对于C语言程序处理一般是以下几步,预处理-编译-汇编-链接。
内嵌汇编
为了高效运行C语言函数,可能会在函数体中加入汇编命令,虽然这很少见。其基本格式:
asm(“汇编语句”
:输出寄存器
:输入寄存器
:会被修改的寄存器
);
寄存器变量
我们可以使用CPU的寄存器来存储一些变量,gcc数据流分析可以确定指定寄存器中何时含有正在使用的值,何时可以派上其他用场。若不想gcc做出优化,最好使用volatile关键字。
内联函数
我们可以通过对函数声明为内联inline,从而可以让gcc把函数集成在调用的位置里去,可以节省调用和返回的开销。
C语言与汇编程序的相互调用
首先说明C语言的函数调用方法
函数的调用操作包括一块代码和另一块代码的双向数据传递和执行控制转移,数据传递就是函数的参数和返回值。另外需要在进入函数时分配一部分用于储存局部变量的空间,intel80x86提供简单的指令,空间分配和回收都通过栈操作实现。
需注意,在调用时调用者需要保存eax,edx,ecx的值,被调用者需要保存ebx,esi,edi的值。
main也是一个函数,将作为crt0.s的函数调用,该程序目标程序将被链接在每个用户执行程序的开始部分,主要用于初始化一些全局变量。
汇编语言调用C函数
调用时,将C函数的参数逆向顺序压入栈中,最后一个参数在栈底,第一个参数在栈顶,然后执行call,返回后在把先前压入栈中的数据清除,在call时,将call下一条指令的地址压入栈中以便在返回时可以直接进入调用处。
调用时参数比较随意,比如可以只压入3个参数,但需要远超过3个参数,这时候就是在第四个参数开始继续使用第三个参数之前的栈中的内容。
更自由的是调用时可以不用call指令,用jmp指令就可以完成,在ret后把人工压入栈中的下一条指令弹出,作为函数返回的地址。内核中很多地方用到了这种方法。
C程序中调用汇编函数
与上述原理相同。
Linux 0.12 目标文件格式
编译器产生目标模块文件,链接器产生可执行文件,将其统称为目标文件。都采用UNIX的a.out文件格式,包括文件头,正文段,数据段,重定向信息区,符号表和符号名字符串。
执行头部分:包含一个32B的exec数据结构,包括魔数,代码和数据的长度,未初始化区的长度,符号表的长度,执行开始的地址,重定位信息的长度。魔数在编译生成的模块目标文件中是Omagic 为0x0107,在需求分页处理的可执行文件中是Zmagic 为0x010b,另外zmagic的文件头要留出1kb的文件头,32B后其余位为0。
代码段(正文段):保存二进制指令代码和数据信息,含有被加载到内存上的指令代码和相关数据,以只读方式加载.
数据段:总是被加载到可读写的内存中。
重定位信息部分:只有模块文件包含用于链接的重定位信息。重定位的功能有两个,当代码段被重定位到另一个不同的基地址处时,重定位项指出需要修改的地方,二是模块文件中存在对未定义符号的使用时,当其最终被定义时,链接文件就可以利用重定位信息对符号的值进行修正。
符号表和字符串部分:包含了字符串的的偏移位置,和符号类型,类型包括3种,一是text,data或bbs指明是本模块文件中定义的符号,值是该符号的重定位地址,二是abs,指明是绝对的,补课重定位的,值就是该固定值,三是undef,指明是未定义符号,值通常是0。
链接程序输出
链接程序的主要任务就是给执行文件进行存储空间的分配操作
其次,链接时把所有模块文件中的相同类型的段结合在一起在输出文件中为指定段类型形成一个单一段。
链接程序预定义变量
链接器ld和ld86都会使用变量记录执行程序的每个段的逻辑地址、在程序中可以访问这些变量来获取程序中段的位置。预定义的外部变量通常有etext, _etext, (正文段结束后的第一个地址) edata, _edata, (初始化数据段结束后的第一个地址)end, _end(未初始化数据段bss结束后的第一个地址).
system.map文件
利用该文件,程序可以通过已知的内存地址查找出内核变量名称,便于对内核的调试工作。
文件中包含
1. 目标文件及符号表在内存中的位置
2. 公共符号如何放置
3. 链接中包含的所有文件成员和引用的符号
Make和Makefile
make程序用来编译较繁杂的文件,可以自动的确定在大型程序中哪些文件需要重新编译。Makefile文件主要包括make需要遵守的执行规则和执行的指令。
Makefile的内容
包括五种元素:显式规则,隐含规则,变量定义,指示符和注释。
显式规则:
target:prerequisites
command
…
…
target是目标文件,可以是可执行文件也可以是.o文件,prerequisites是先决条件也是依赖的关系,即target依赖的源文件,command是make执行的指令,通常是一些shell命令,每个命令占一行,而且每个命令之前都有一个TAB制表符!有的规则可以没有先决条件,比如clean为target的命令,规则执行时如果有依赖关系文件比目标文件更新,就要重新编译生成目标文件。
变量定义:
在目标文件的依赖非常多的时候,每次都重新输入容易出错,并且不方便更正,所以可以定义一个obj变量,再引用时用$(obj)即可。
隐含规则:
如果没有写出命令,那么make程序会自动根据依赖关系生成相应的.o文件,而且在依赖关系中也不用写出相同名字的.c文件,比如只写main.o:defs.h
make会将main.c和defs.h一起编译生成main.o
这样Makefile就简洁了许多。
还有隐含规则包括一些自动变量,比如$<表示第一个先决条件,加^表示所有先决条件,加@表示目标文件等等。
注释
Makefile中的注释前有#,可以加在任何地方
指示符
指在读取Makefile时的特定操作,可以包括读取另一个Makefile,确定使用或忽略其中某部分内容和从包含多行的字符串中定义变量。