第3章 内核编程语言与环境(1)
这一章都是基础内容,但是感觉非常重要,填补了我之前好多的知识空白。
可先大致浏览原书内容,然后碰到问题回过头来再看。
目录
- 第3章 内核编程语言与环境(1)
- 3.1 as86汇编器
- 3.2 GNU as 汇编
- 3.2.1 编译as汇编语言程序
- 3.2.2 as汇编语法
- 3.2.3 指令语句、操作数和寻址
- 3.2.4 区与重定位
- 3.2.5 符号
- 3.2.6 as汇编命令
- 3.2.6.1 .align abs-expr1,abs-expr2,abs-expr3
- 3.2.6.2 .ascii “string”
- 3.2.6.3 .asciz "string"
- 3.2.6.4 .byte expressions
- 3.2.6.5 .comm symbol,length
- 3.2.6.6 .data subsection
- 3.2.6.7 .desc symbol,abs-expr
- 3.2.6.8 .fill repeat,size,value
- 3.2.6.9 .global symbol (或.globl symbol)
- 3.2.6.10 .int expressions
- 3.2.6.11 .lcomm symbol,length
- 3.2.6.12 .long expressions
- 3.2.6.13 .octa bignums
- 3.2.6.14.org new_lc,fill
- 3.2.6.15 .quad bignums
- 3.2.6.16 .short expressions(同.word expressions)
- 3.2.6.17 .space size,fill
- 3.2.6.18 .string "string"
- 3.2.6.19 .text subsection
- 3.2.6.20 .word expressions
- 3.2.7 编写16位代码
- 3.2.8 AS汇编器命令行选项
- 3.3 C语言程序
3.1 as86汇编器
3.1.1 as86汇编语言语法
as86汇编器以及对应的ld86链接器用于生成16位代码和实模式下的代码。即用于编写16位的启动引导扇区程序boot/bootsect.s以及实模式下的初始设置程序boot/setup.s
32位代码用GNU as汇编器和ld链接器编写。
3.1.2 as86汇编语言程序
!
! boot.s -- bootsect.s的程序框架。用代码0x07替换串msg1中1字符,然后在屏幕第1行显示
!
.global begtext,begdata,begbss,endtext,enddata,endbss !全局标识符,供ld86链接使用;
.text !正文段;
begtext:
.data !数据段;
begdata:
.bss !未初始化数据段;
begbss:
.text !正文段;
BOOTSEG = 0x07c0 !BIOS加载bootsect代码的原始段地址;
entry start !告知链接程序,程序从start标号处开始执行。
start:
jmpi go,BOOTSEG !段间跳转。BOOTSEG指跳转段地址,标号go是偏移地址
go: mov ax,cs !段寄存器cs值-->ax,用于初始化数据段寄存器ds和es
mov ds,ax
mov es,ax
mov [msg1+17],ah !0x07-->替换字符串中1个点符号,喇叭将会鸣一声。
mov cx,#20 !共显示20个字符,包括回车换行符
mov dx,#0x1004 !字符串将显示在屏幕第17行、第5列处
mov bx,#0x000c !字符显示属性(红色)
mov bp,#msg1 !指向要显示的字符串(中断调用要求)
mov ax,#0x1301 !写字符串并移动光标到串结尾处。
int 0x10 !BIOS中断调用0x10,功能0x13,子功能01
loop1: jmp loop1 !死循环
msg1: .ascii "Loading system ..." !调用BIOS中断显示的信息。共20个ASCII码字符
.byte 13,10 !!回车和换行
.org 510 !表示以后语句从地址510(0x1FE)开始存放
.word 0xAA55 !有效引导扇区标志,供BIOS加载引导扇区使用。
.text
endtext:
.data
enddata:
.bss
endbss:
.org 510是因为引导扇区大小为512字节,这样设置之后512字节的最后两个字节为 0xAA,0x55,即为有效引导扇区标志。
由于本程序无分段需求,4-11行以及32-37行完全可以省略。
3.1.3 as86汇编语言程序的编译和链接
as86 -0 -a -o boot.o boot.s #出现as: error reading input 错误,用vi打开再关闭即可解决
ld86 -0 -s -o boot boot.o
dd bs=32 if=boot of=/dev/fd0 skip=1 #用于去除多余的32字节
自己电脑上的执行结果:
dd bs=32 if=boot of=/dev/fd0 skip=1
#dd:convert and copy a file,转化复制文件
#描述:Copy a file, converting and formatting according
# to the operands. 复制文件,并根据操作数进行转化和格式化。
#bs:bs=BYTES read and write up to BYTES bytes at a time (default: 512);
# overrides ibs and obs.每次读和写最多BYTES字节(默认:512);覆盖ibs和
# obs
#if:if=FILE read from FILE instead of stdin 读取文件而不是用标准输入
#of: of=FILE write to FILE instead of stdout 写入文件而不是标准输出
#skip: skip=N skip N ibs-sized blocks at start of input 在输入开始时跳过
# N个ibs块的大小
#按照上面的指令最后的文件为/usr/fd0
#修改为如下指令可以在本地生成myImage
dd bs=32 if=boot of=myImage skip=1
参考Ubuntu18.10安装bochs,在ubuntu环境下,编写bochs2.7.1配置文件bochssrc.txt如下:
#模拟器的内存
megs:128
#BIOS-bochs-latest的路径
romimage:file=/usr/local/share/bochs/BIOS-bochs-latest
#VGABIOS-lgpl-latest的路径
vgaromimage:file=/usr/local/share/bochs/VGABIOS-lgpl-latest
#启动软盘,1_44后面就是我们下载的linux0.11镜像文件
floppya:1_44=myImage,status=inserted
#表示从软盘启动
boot:floppy
#日志输出文件
log:bochsout.txt
#友情提示不要设置为1
mouse: enabled=0
然后输入指令bochs -f bochsrc.txt,并在终端输入c,则可得到:
第17行第5列,红色的“Loading system …”
为了和书中保持一致,使用windows系统以及书中资料给出的bochs版本(参见第17章 实验环境设置与使用方法)。
结果如下图,并可听到一声蜂鸣器声响,对应代码第20行。不知道是不是因为linux使用的是虚拟机,linux下并没有听到声响,而且linux下最后的圆点还在,只是颜色变白了而已。
书上给出的结果图似乎不是17行第5列。
3.1.4 as86和ld86使用方法和选项
可通过man as86 以及 man ld86随时查看。
3.2 GNU as 汇编
3.2.1 编译as汇编语言程序
objfile:编译输出的目标文件名
srcfile.s as的输出汇编语言程序名
单独编译boot/head.s汇编程序:
参考ubutu14 下编译linux0.11内核错误记录及解决方法在64位机器上进行编译需要加上–32选项,即:
as --32 -o head.o head.s
最后编译出来大小和书上的并不一致。
3.2.2 as汇编语法
之前看过一点王爽《汇编语言》,里面用的是微软的MASM编译器,使用的应该是Intel汇编程序使用的语法。Linux下更多是AT/T汇编语法。
3.2.2.1 汇编程序预处理
3.2.2.2 符号、语句和常数
符号(Symbol):由大小写字符集、数字和“_.$”组成;不能以数字开头;使用其他字符(空格、换行)以及文件的开始来确定开始和结束
语句(Starement):以换行或者“;”作为结束;“\”和换行用于连接不同行;前面有0个或多个标号(Label)。
常数:分为字符常数和数字常数。字符可以分为单个字符和字符串;数字常数包括整数、大数(位数超过32位二进制的数)和浮点数。
字符串必须用双引号括起来。
常用的转义字符:
3.2.3 指令语句、操作数和寻址
指令(instruction):包括标号(可选)、操作码(指令助记符)、操作数、注释四个部分组成
这个和intel汇编不一样。
操作数:可以为立即数、寄存器或内存。
3.2.3.1 指令操作码的命名
这点也和intel汇编不一致。
3.2.3.2 指令操作码前缀
3.2.3.3 内存引用
3.2.3.4 跳转指令
3.2.4 区与重定位
区(Section):至少三个正文(text)、数据(data)、和bss区。
重定位:
C语言程序的内存分布?
3.2.4.1 链接器涉及的区
ld涉及四类区:
text区,data区,(通常text区数据不会改变,而data区数据会变化,如C语言中的变量一般在data区)
bss区:保存未初始化变量或公共变量
absolute区:可看做“不可重定位的”
undefined区:不在上面的区里面的
链接就类似于归类?把不同的目标文件中的相同类的区放到一起。
3.2.4.2 子区
在每个区中,可以有编号为0-8192的子区存在。将汇编源程序某个区中不相邻的数据组在汇编后聚集在一起存放。
3.2.4.3 bss区
在编译、汇编、链接完成后,程序分为如上几段。
变量在程序运行时的cpu看来就是一个地址,保存在data段的地址。在程序中凡是涉及到对这个变量的操作,都变成对这个地址的操作,实际的物理上就是内存上的一块存储区域。
指针也应该是data段的块内存区域,只不过它里面保存的是指向变量的地址。应该从汇编的角度去尝试理解指针等一些操作会比较容易。参见从汇编角度分析C语言指针
3.2.5 符号
标号(Label):后面紧跟着一个冒号的符号,代表当前活动位置计数器的当前值。
3.2.5.1 特殊点符号
3.2.5.2 符号属性
除了名字意外,每个符号都有“值”和“类型”属性
3.2.6 as汇编命令
3.2.6.1 .align abs-expr1,abs-expr2,abs-expr3
.align:储存地址对齐汇编命令,在当前子区中把位置计数器值增加到下一个指定存储边界处。
abs-expr1(absolute expression):指定要求的边界对齐值,一般为2的次方值。对使用a.out目标格式的80X86z系统,
对使用ELF格式的80X86系统:
abs-expr2:用于对齐而填充的字节值。可省略,默认为0
abs-expr3:对齐操作允许填充跳过的最大字节数。
3.2.6.2 .ascii “string”
为字符串分配空间并存储字符串。多个字符串时,每个字符串后面不会自动添加NULL。
3.2.6.3 .asciz “string”
与 .ascii 类似,但是多个字符串时,每个字符串后面会自动添加NULL。
3.2.6.4 .byte expressions
定义0个或多个用逗号分开的字节值
3.2.6.5 .comm symbol,length
在bss区中声明一个命名的公共区域。
3.2.6.6 .data subsection
把随后的语句汇编到编号为subsection的data子区中,默认编号为0。
3.2.6.7 .desc symbol,abs-expr
参见有关include/a.out.h 文件的说明
3.2.6.8 .fill repeat,size,value
产生repeat个大小为size字节的重复拷贝。size默认值为1,value默认值为0
3.2.6.9 .global symbol (或.globl symbol)
使得连接器ld可以看见符号symbol,全局符号。
3.2.6.10 .int expressions
在某个区中设置0个或多个整数值
3.2.6.11 .lcomm symbol,length
为符号symbol指定的局部公共区域保留长度为length字节的空间。
3.2.6.12 .long expressions
与.int相同
3.2.6.13 .octa bignums
在某个区中设置0个或多个16字节大数
1字节 .byte 2字节 .word 四字节 .long 八字节 .quad 16字节 .octa
3.2.6.14.org new_lc,fill
把当前区的位置计数器设置为值new_lc。中间跳过的部分用fill填充,fill默认为0。
3.2.6.15 .quad bignums
在某个区中设置0个或多个8字节大数
3.2.6.16 .short expressions(同.word expressions)
在某个区中设置0个或多个2字节数
3.2.6.17 .space size,fill
产生size个字节,每个字节填充 fill,fill值默认为0
3.2.6.18 .string “string”
定义1个或多个字符串,默认自动加NULL
3.2.6.19 .text subsection
把随后的语句汇编到编号为subsection的子区中,默认值为0
3.2.6.20 .word expressions
对32位机器,同.short expressions
3.2.7 编写16位代码
3.2.8 AS汇编器命令行选项
3.3 C语言程序
3.3.1 C程序编译和链接
gcc 编译C语言程序时四个阶段:预处理、编译、汇编、链接。
不同阶段做的事情:
gcc指令格式
gcc [选项] [-o outfile] infile ...
infile:输入的C语言文件
outfile:编译产生的输出文件
其他使用形式
3.3.2 嵌入汇编
具有输入和输出参数的嵌入式汇编语句的基本格式为:
asm("汇编语句"
:输出寄存器
:输入寄存器
:会被修改的寄存器);
除第一行外,后面不使用则可以省略
asm: 内联汇编语句关键词
“汇编语言”: 汇编指令
实例,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;})
嵌入式汇编语言宏函数,({})中为表达式,__res为结果。‘\’将这些语句连成1行。
宏语句要定义在一行上。
该函数的功能是从指定段和偏移值的内存地址处取一个字节。
:“0” (seg),“m”(*(addr)))\ //输入寄存器列表 一句中“0”代表使用与上面通个位置的输出相同的寄存区,因此seg使用eax寄存器,因此在语句mov %%ax,%%fs;中ax的值就是seg的值;
按照寄存器编号规则,代表(*(addr))中的值的寄存器为%2,因此movb %%fs:%2,%%al中的%2即为内存偏移量。
a、m等寄存器加载代码及其含义见表3-4
第二个例子:
asm("cld\n\t"
"rep\n\t"
"stol"
:/*没有输出寄存器*/
:"c"(count-1),"a"(fill_value),"D"(dest)
:"%ecx","%edi");
1-3行是通常的汇编语句,用以清方向位,重复保存值。"\n\t"是用于gcc预处理程序输出列表好看设置的。
第5行 将count-1的值加载到ecx中,fill_value加载到eax中,dest放到edi中。
第6行 告诉gcc这些寄存器中的值已经改变了。
寄存器加载代码及其具体含义:
第三个例子 不指定哪个变量用哪个寄存器,让gcc选择
asm("leal (%1,%1,4),%0"
:"=r"(y)
:"0"(x));
假设gcc将r指定为eax的话,上面代码含义为
"leal (eax,eax,4),eax"
在执行代码时,如果不希望汇编语句被gcc优化,可以添加关键词volatile
asm volatile(......);
或者更详细的说明为:
__asm__ __volatile__(......); //建议使用这种形式
volatile放在函数名前来修饰函数,用于通知gcc编译器该函数不会返回。
较长的例子:strncmp函数的实现
字符串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,则返回-1。
extern inline int strncmp(const char * cs,const char * ct,int count)
{
register int __res; //__res是寄存器变量。
__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 lb\n" //不是,则向后跳转到标号1,继续比较
"2:\txorl %%eax,%%eax\n\t" //是NULL字符,则eax清零(返回值)
"jmp 4f\n\t" //向前跳转到标号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.3.3 圆括号中的组合语句
语句表达式形式:
({ int y=foo();int z;
if(y > 0) z = y;
else z=-y;
3+z;})//3+z为整个圆括号扩住的语句的值
类似于
res = x + ({略...}) + b;
这种表达值常用于定义宏,如读取CMOS时钟信息的宏定义:
#define CMOS_READ(addr)({\ //最后反斜杠起连接两行语句的作用
outb_p(0x80|addr,0x70);\ //首先向I/O端口0x70输出欲读取的位置addr
inb_p(0x71);\ //然后从端口0x71读入该位置处的值作为返回值。
})
读I/O端口port的宏定义
#define inb(port) ({ \
unsigned char _v;\
__asm__ volatile("inb %%dx,%%al":"=a"(_v):"d"(port));\
_v;\ //inb()的返回值
})
3.3.4 寄存器变量
寄存器变量:GCC允许我们把一些变量的值放到CPU寄存器中。可分为全局寄存器变量和局部寄存器变量。linux内核中通常只使用局部寄存器变量,其使用形式为:
register int res __asm__("ax") //ax是变量res希望使用的寄存器
3.3.5 内联函数
内联(inline)函数:可以让gcc把函数的代码集成到调用该函数的代码中去,以减少函数调用时的进入/退出时间开销。编译时需要加入优化选项“-O”。
内联函数关键“inline”,其声明例子如内核文件 fs/inode.c:
inline int inc(int *a)
{
(*a)++;
}
编译时 -Winline选项 让gcc对标志成inline但不能被替换的函数给出警告信息以及不能替换的原因。
static + inline 例子 fs/inode.c
static inline void wait_on_inode(struct m_inode * inode)
{
cli();
while(inode->i_lock)
sleep_on(&inode->i_wait);
sti();
}
ISO标准c99中的内联函数功能相当于C89的static inline
inline + extern:表示该函数仅用于内联集成,在任何情况下都不会单独产生该函自身的汇编代码。实例:linux 0.1x内核源代码中文件 include/string.h、lib/string.c。
//将字符串(src)拷贝到另一字符串(dest),直到遇到NULL字符后停止
//参数:dest - 目的字符串指针 , src - 源字符串指针 。 %0 - esi(src), %1-edi(dest).
extern inline char * strcpy(char* dest,const char *src)
{
__asm__("cld\n" //清方向位
"1:\tlodsb\n\t" //加载DS:[esi]处1字节->al,并更新esi
"stosb\n\t" //存储字节al->ES:[edi],并更新edi
"testb %%al,%%al\n\t"//刚存储的字节是0?
"jne 1b" //不是则向后跳转到标号1处,否则结束
::"S"(src),"D"(dest):"si","di","ax");
return dest; //返回目的字符串指针
}
#define extern //定义为空
#define inline //定义为空
#define LIBRARY
#include <string.h>
此时库函数中重新定义上述strcpy()函数变成如下形式:
char * strcpy(char* dest,const char *src) //去掉了关键词inline和extern
{
__asm__("cld\n" //清方向位
"1:\tlodsb\n\t" //加载DS:[esi]处1字节->al,并更新esi
"stosb\n\t" //存储字节al->ES:[edi],并更新edi
"testb %%al,%%al\n\t"//刚存储的字节是0?
"jne 1b" //不是则向后跳转到标号1处,否则结束
::"S"(src),"D"(dest):"si","di","ax");
return dest; //返回目的字符串指针
}
大多数可替换的会直接替换,不可替换的则引用string.c中的拷贝