Linux-0-11-内核编程语言和环境笔记

as86汇编器

在Linux0.11中,有两种汇编语言程序,分别是能产生16位代码的as86汇编器,使用配套的ld86链接器。另一种是GNU的汇编器gas(现在改名为as)
,使用GNU ld链接器来链接目标文件。

在Linux0.11中,使用了as86和ald86来创建16位的启动引导扇区程序boot/bootsect.s和实模式下初始化设置程序boot/setup.s的二进制编码。

语法

汇编器专门用来把低级汇编语言翻译成含有机器码的二进制程序或者目标文件。汇编的命令行基本格式:

1
as [选项] -o objfile srcfile

源文件是由换行字符结尾的一些列文本行组成的,虽然可以使用分号在一行上包含多个行,但通常汇编语言程序每行只包含一行语句。

语句可以使空行,也可以是赋值语句、伪操作符语句和机器指令语句。

  1. 赋值语句。例如 bootset = 0x700就是一个赋值语句
  2. 伪操作符语句。通常不会产生任何代码,仅仅是汇编器使用的指示符,由伪操作码和0个或者多个操作数组成。每个操作码都是有点符号.开始。
  3. 机器指令语句。机器指令语句是可执行机器指令的助记符,由操作码和0个或者多个操作数构成。可以有标号,也可以没有。

汇编器产生的目标文件objfile通常至少包含三个段或区,即正文段.text,数据段.data和未初始化数据段.bss。

  1. .text 是一个已经初始化过的段,通常其中包含程序的执行代码和只读数据。
  2. .data 也是一个已经初始化过的段,通常包含有可读可写的数据。
  3. .bss 是一个未初始化的段。在执行程序被加载的时候,该段的内容被全部初始化成0。

as86汇编语言的编译和链接

1
2
3
as86 -0 -a -o boot.o boot.s
ld86 -0 -s -o boot boot.o
dd bs=32 if=boot of=/dev/fd0 skip=1

GNU as 汇编

除去之前的两个汇编文件,内核中的其他汇编程序均由gas来编译,并与C语言程序编译产生的模块链接。以下简称gas为as。

略。

区和重定位

区表示了一个地址范围,操作系统将会以相同的方式对待和处理在该地址范围中的数据信息。例如,对于只读区,我们只能从该区中读取数据而不能写入。

链接器ld会把输入的目标文件中的内容按照一定的规律组合成一个可执行程序。当as汇编器输出一个目标文件时,该目标文件中的代码被默认设置成从地址0开始。此后ld将会在链接过程中为不同的目标文件的各个部分分配不同的最终地址。ld会把程序中的字节块移动到程序运行时的地址处。这些块是作为固定单元进行移动的,他们的长度以及字节序都不会改变。这样的固定单元就被称作是区。

为区分配运行时刻的地址的操作j就被称为重定位操作。

在一个区被重定位时,为了让链接器ld知道哪些数据会发生变化以及如何修改这些数据,as汇编器也会往目标文件中写入所需要的重定位信息。为了执行重定位操作,在每次涉及目标文件的一个地址时,ld必须知道:

  1. 目标文件对一个地址的引用是从什么地方算起的?
  2. 该引用的字节长度是多少?
  3. 该地址引用的是哪个区?值等于多少?
  4. 对地址的引用与程序计数器pc相关吗?

绝对地址区(absolute 区)。当链接器把各个目标文件组合在一起时,absolute区中的地址始终不变。尽管链接器在链接后绝不会把两个目标文件的data区安排在重叠的地址上,但是目标文件的absolute区必会重叠而覆盖。

另外还有一个未定义区

子区

略。

汇编指令

  1. .align expr1 expr2 expr3。.align是汇编对齐指令,用于在当前子区中把位置计数器值设置到下一个指定存储边界处。
  2. .ascii “String”。 从位置计数器当前位置为字符串分配空间并存储字符串。可以逗号分开写出多个字符串。不会在每个字符串后添加0。
  3. .asciz “String”。 同上,但是会在每个字符串后添加null字符。
  4. .byte expressions。该汇编命令定义0个或者多个用逗号分开的字节值。每个表达式的值是一个字节。
    略。

嵌入汇编

C语言编程时可以在代码中内嵌汇编代码其格式为:

1
2
3
4
5
6
asm(
"汇编语句"
: 输出寄存器
: 输入寄存器
: 会被修改的寄存器
);

除了第一行,后面带冒号的行都是可以被省略的。

寄存器变量

GNU对C语言的另一个扩充时允许我们把一些变量值放到CPU寄存器中,即所谓寄存器变量。这样CPU就不用花费较长时间访问内存区取值。寄存器变量分为两种:

  1. 全局寄存器变量
  2. 局部寄存器变量

内联函数

在程序中,通过把一个函数生命为inline,内联函数,就可以让gcc把函数代码集成到该函数的代码中去。这样处理可以去掉函数调用时进入和退出的开销,从而加快执行速度。

C语言与汇编程序的相互调用

c函数调用机制

在Linux内核程序boot/head.s执行完基本初始化操作之后,就会跳转执行init/main.c程序。那么head.s是如何把执行控制交给init/main.c程序的呢?

  1. 栈帧结构和控制转移权方式。大多数CPU上的程序都是使用栈来支持函数调用操作的。栈被用来存储函数参数、存储返回信息、临时保存寄存器原有值以及存储局部数据。单个函数调用操作所使用的栈部分被称位栈帧机构。如下:

栈帧的两端由两个指针来指定。寄存器ebp通常用作帧指针,而esp则用作栈指针。栈指针esp会随着数据的入栈和出栈而移动,因此函数中对大部分数据的访问都基于帧指针ebp进行。

对于函数A调用函数B的情况,传递给B的参数包含在A的栈帧中。当A调用B时,函数A的返回地址(调用结束后继续执行的指令地址)被压入栈中,栈中该位置也明确指明了A栈帧的结束处。而B的栈帧则从随后的栈部分开始,即图中保存帧指针(ebp)的地方开始。再随后则用于存放任何保存的j寄存器的值以及函数的临时之。

栈是往低地址方向扩展的。而esp指向当前栈顶处的元素。通过使用push和pop指令我们可以将数据压入栈中或从栈中弹出。

指令call和ret用于处理函数调用和返回操作。

  1. 调用call指令程序将返回地址压入栈中并调转到被调用函数处开始执行。返回地址是程序中紧跟指令call后面的一条指令的地址。
  2. 返回指令ret用于弹出栈顶的地址并跳转到该地址处。使用该指令之前,需要先正确处理栈中的n内容,使得当前栈指针所指位置内容正是先前call指令保存到的返回地址。
  3. 若返回值是一个正数或者指针,那么j寄存器eax将被默认用来传递返回值。

略。

目标文件格式

略。

Make程序和Makefile文件

make工具程序的主要用途是能自动地决定含有很多源程序文件的大型程序中哪些文件需要被重新编译,makefile是make工具程序的配置文件。