Post

内核编程语言和环境

内核编程语言和环境

1. as86汇编器

  • Linux系统用它创建16位的启动引导扇区程序boot/bootsect.s和实模式下初始设置程序boot/setup.s的二级制执行代码
  • 使用Intel的汇编语言语法(与GNU不同)
    • 基于MINIX系统的汇编语言语法
  • 汇编器把低级汇编语言程序编译成含机器码的二进制程序或目标文件
  • as [option] -o objfile srcfile

语法

标号(可选)、指令助记符(指令名)和操作数

  • 语句可以只包含空格、制表符和换行符的空行,
  • 赋值语句
  • 伪操作符语句
    • .操作码 0个或多个操作数
  • 机器指令语句
  • 任何语句之前都可以有标号
    • 标识符+:

目标文件objfile

  • 通常包含至少三个段或区(section)
    • 正文段(.text)
      • 已初始化的段
      • 包含程序的执行代码和只读数据
    • 数据段(.data)
      • 已初始化的段
      • 包含可读/写的数据
    • 未初始化数据段(.bss)
      • 编译器不会为该段保留空间,但在链接称执行程序被加载时,操作系统会把该段的内容全部初始化为0

as86汇编语言程序

  • 命令
    • 编译命令:as86 -O -a -o boot.o boot.s
    • 链接命令:ld86 -O -s -o boot boot.o

2. GNU as 汇编(gas)

  • Linux内核中的其余所有汇编语言程序均使用gas来编译,并与C语言程序编译产生的模块链接
  • as汇编器也支持许多C语言特性
  • 使用AT&T系统V的汇编语法
  • as [option] [-o objfile] [srcfile.s ...]

语法

  • Intel语法和AT&T语法的差异
    1. AT&T语法中立即操作数前要加’$$$’;寄存器操作数名前要加’$\%$’;绝对跳转/调用操作数前加’*’
    2. 源和目的操作数次序正好相反。AT&T的源和目的操作数是从左到右为”源、目的“
    3. AT&T语法中的内存操作数的长度由操作码最后一个字符来确定:’b’、’w’和’l’;Intel语法使用前缀来到达同样的目的
    4. 远跳转和远调用、远返回指令的差异
    5. AT&T汇编器不提供对多代码程序的支持,UNIX类操作系统要求所有代码在一个段中
  • 汇编程序预处理(指的是as汇编器)
    • 调整并删除多余的空格字符和制表符;
    • 删除所有注释语句并使用单个空格或一些换行符替换它们;
    • 把字符常数转换称对应的数值
    • 但是不会对宏定义处理、也没有处理包含文件的功能
  • 符号:由字符组成的标识符
  • 语句:由零个或多个标号开始,以换行符或者行分割字符(‘$;$’)作为结束
    • 标号:汇编命令 注释部分(可选)
    • 标号:指令助记符 操作数1,操作数2 注释部分(可选)
  • 常数:字符常数和数字常数

指令语句、操作数和寻址

  • 指令语句
    • 标号(可选)
    • 操作码(指令助记符)
    • 操作数
      • 立即数、寄存器、内存
    • 注释
  • 操作码
    • 后缀:指明操作数的宽度,包括字符‘b’、’w’、’l’
    • 前缀:用于重复字符串指令、提供区覆盖、执行总线锁定操作、或指定操作数和地址宽度
      • 例如:重复操作的的串扫描指令 repne scas %es:(%edi), %al
  • 内存引用
    • Intel:section:[base + index*scale + disp]
    • AT&T:section:disp(base, index, scale)
    • base是32位基寄存器;index是索引寄存器;disp是可选的偏移值;scale是比例因子;section为内存操作数指定可选的段寄存器
  • 跳转指令
    • 条件跳转
      • 依赖于执行指令时标志寄存器中某个相关标志的状态来确定是否进行跳转
      • 只有直接跳转指令
    • 无条件跳转JMP
      • 直接跳转
        • 给出跳转目标处的标号
      • 间接跳转
        • 跳转位置取自某个寄存器或内存位置

区与重定位

  • :用于表示一个地址范围,操作系统将会以相同的方式对待和处理在该地址范围中的数据信息
  • 链接器ld工作流程
    • ld会把输入的目标文件的内容按照一定规律组合生成一个可执行程序
    • 目标文件中的代码默认设置成从地址0开始
    • ld在链接过程中为不同目标文件中的各个部分分配不同的最终地址位置
  • 重定位:为程序分配运行时刻的地址的操作
  • bss区
    • 用于存储局部公共变量
    • 程序刚开始执行时,bss区中所有字节内容都将被清零
  • 绝对地址区
    • 该区的地址始终不变
    • 该区的地址0总是”重定位“到运行时刻地址0处
  • “未定义的“区
    • 在汇编时不能去顶所在区的任何地址
    • 涉及一些未定义的符号

链接

符号

  • 链接器使用符号进行连接操作;调试器利用符号进行调试

as汇编命令

  • .align:对齐
  • .ascii "string"...
  • .global symbol
  • int expressions
  • long expressions
  • ……

3. C语言程序

C程序编译和链接

gcc汇编器编译C语言程序的四个处理阶段 gcc [option] [-o outfile] infile... C程序编译过程

  • 预处理阶段
    • cpp对C语言程序中指示符和宏进行替换处理,输出纯C语言代码
  • 编译阶段
    • gcc -S -o hello.s hello.c
    • gcc把C语言程序编译生成对应的与机器相关的as汇编语言代码
  • 汇编阶段
    • gcc -c -o hello.o hello.c
    • as把汇编代码转换成机器指令,并以特定的二进制格式输出保存在目标文件中
  • 链接阶段
    • ld把程序的相关目标文件组合链接在一起,生成程序的可执行映像文件
  • 通常使用make工具软件对整个程序的编译过程进行自动管理

嵌入汇编

1
2
3
4
asm("汇编语句"
	: 输出寄存器
	: 输入寄存器
	: 会被修改的寄存器);
  • 关键词volatile通知gcc编译器该函数不会返回

圆括号中的组合语句

  • 形如”({……})”的语句
  • 最后一条语句的值作为该组合语句的值
  • 常用来定义宏

寄存器变量

  • 全局寄存器变量
    • 在程序的整个运行过程中为全局变量而保留的寄存器
  • 局部寄存器变量
    • 不会保留指定的寄存器,而在内嵌asm汇编语句中作为输入或输出操作数时专门使用的寄存器

内联函数

  • 作用:让gcc把内联函数的代码集成到调用该函数的代码中去,从而去掉函数调用的时间开销,加快执行速度
  • 使用关键词inline声明
  • 一般是一种优化操作,只有使用-O选项才能做到代码内嵌

4. C与汇编程序的相互调用

C函数调用机制

  • 在堆栈上临时存放被调函数参数的值 栈帧结构
  • 栈指针esp会随着数据的入栈和出栈而移动,因此函数中对大部分数据的访问都基于帧指针ebp进行
  • 指令CALL:处理函数调用
    • 把返回地址压入栈中并跳转到被调用函数开始处执行
  • 指令RET:处理返回操作
    • 弹出栈顶处的地址并跳转到该地址处
  • 寄存器用法统一惯例
    • 寄存器eax、edx和ecx有调用者保护
    • 寄存器ebx、esi、edi、ebp和esp由被调用者保护

在汇编程序中调用C函数

调用函数时压入堆栈的参数

  • 将被调用函数的所需参数入栈,最右边的参数最先入栈
  • 然后执行CALL指令(或者使用JMP也可以,不过需要先把下一条要执行的指令地址压入栈中)

在C程序中调用汇编函数

  • 原理与汇编程序中调用C函数的原理相同,重点在于确定函数参数在栈中的位置

5. Linux0.11 目标文件格式

  • gcc或gas编译输出的目标模块文件和链接程序所生成的可执行文件都使用了UNIX传统的a.out格式
    • 有利于内存分页机制的系统

a.out格式的目标文件

  • 执行头部分
    • 含有一些参数,是有关目标文件的整体结构信息
    • 含有32字节的exec数据结构,如下
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      struct exec{
        unsigned long a_magic    // 执行文件魔数(OMAGIC、ZMAGIC)
        unsigned a_text          // 代码长度
        unsigned a_data          // 数据长度
        unsigned a_bss           // 文件中的未初始化数据区长度
        unsigned a_syms          // 文件中的符号表长度
        unsigned a_entry         // 执行开始地址
        unsigned a_trsize        // 代码重定位信息长度
        unsigned a_drsize        // 数据重定位信息长度
      }
      // 长度单位都为字节数
      // 可执行文件并不需要符号表和重定位信息,所以这些信息字段的值通常为0
      
  • 代码区
    • 含有程序执行时被加载到内存中的指令代码和相关数据
  • 数据区
    • 含有已经初始化过的数据,总是被加载到可读写的内存中
  • 重定位部分
    • 代码段和数据段的重定位信息均有重定位记录项构成,每个记录的长度为8字节
1
2
3
4
5
6
7
8
struct relocation info{
	int r_address;       // 段内需要重定位的地址
	unsigned int r_symbolnum:24;  // 含义与r_extern有关。指定符号表中一个符号或者一个段
	unsigned int r_pcrel:1;     // 1bit,PC相关标志
	unsigned int r_length:2;    // 2bit,指定要被重定位字段长度
	unsigned int r_extern:1;    // 外部标志位。1-以符号的值重定位;0-以段的地址重定位
	unsigned int r_pad:4;       // 没有使用的4个比特位,但最好将它们复位掉
}
  • 符号表部分
    • 保存着模块文件中定义的全局符号以及需要从其他模块文件中输入的符号,或是又链接器定义的符号
  • 字符串部分
    • 含有与符号名相对应的字符串,用于调试程序调试目标代码,与链接过程无关
  • 映射 a.out映射逻辑地址空间
  • Linux0.11系统中进程的逻辑空间大小为64MB
  • 使用需求页技术:一页代码实际要使用的时候才被加载到物理内存页面中
    • 进行加载操作的fs/execve()函数仅仅为其设置了分页机制的页目录项和页表项

链接程序输出

  • 存储空间分配并为所有段分配地址 目标文件的链接操作
  • 符号绑定、代码修正

System.map文件

  • 该文件由linux/Makefile文件产生,用于存放内核符号表信息
    • 符号表是所有内核符号及其对应地址的一个列表
  • 作用:方便其他程序根据已知的内存地址查找处对应的内核变量名称,便于对内核的调试工作

6. Make程序和Makefile文件

  • Makefile文件时make工具程序的配置文件
    • 通过使用makefile文件来告诉make要做哪些工作
      • 通常,该文件会告诉make如何编译和链接一个文件
      • 或者告诉make运行各种命令
  • make工具程序的用途
    • 自动地决定一个含有很多源程序文件的大型程序中哪个文件需要被重新编译
    • 每个修改过从C代码文件必须被重新编译
  • makefile文件含有一些规则
1
2
目标: 先决条件
	命令
  • 目标:程序生成的一个文件名称或者所要采取活动的名字
  • 先决条件:是一个或多个文件名,用作产生目标的输入条件
  • 命令:make需要执行的操作
  • 一个规则可以有多个命令,每个命令自成一行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义编译器和编译选项
CC = gcc
CFLAGS = -Wall

# 定义源文件和目标文件
SRCS = main.c test.c
OBJS = $(SRCS:.c=.o)
TARGET = myprogram

# 默认目标
all: $(TARGET)

# 编译规则
$(TARGET): $(OBJS)
    $(CC) $(CFLAGS) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c -o $@ $<

# 清理规则
clean:
    rm -f $(OBJS) $(TARGET)
符号说明
$@目标文件
$^所有的依赖文件
$<第一个依赖文件(最左边的那个)
@可以抑制命令的输出
This post is licensed under CC BY 4.0 by the author.