kernel源码
0 教材
kernel源码对应的参考教材是《Linux内核完全注释:基于0.11内核(修正版V3.0).pdf》这本书,基于0.11内核。已上传到git私有仓库中
(一)AT&T汇编语法
1 寄存器引用
引用寄存器要在寄存器号前面加%,如mov %eax,%ebx
2 操作数顺序
操作数排列是从源(左)到目的(右),如mov %eax,%ebx
3 常数/立即数的格式
使用立即数,要在数前面加上$,如mov $4,%ebx
符号常数直接引用 如mov value,%ebx
引用符号地址在符号前加$,如mov $value,%ebx
4 操作数长度
操作数长度用加在指令后的符号表示
b(byte),w(word),l(long) 如movw %ax,%bx
5 转移和跳转指令
绝对转移和调用指令(jmp/call)的操作数前要加上*作为前缀。
远转移指令和远调用指令的操作码为ljump和lcall,而intel汇编格式中则为jmp far和call far
比如,AT&T格式
ljump $section,$offset
lcall $section,$offset
Intel格式
jmp far section:offset call far section:offset
远程返回指令
lret $stack_adjust //AT&T格式 ret far stack_adjust //Intel格式
6 寻址方式
section:disp(base,index,scale)表示,计算方法是:base+index*scale+disp
对应的Intel汇编格式:section:[base+index*scale+disp]
例子:
movl -4(%ebp),%eax mov eax,[ebp-4] //ebp减去4放到eax中 movl array(,%eax,4),%eax mov eax,[eax*4+array] //eax*4+array,放到eax中 movw array(%ebx,%eax,4),%cx mov cx,[ebx+4*eax+array] //ebx+4*eax+array,放到cx中 movb $4,%fs:(%eax) mov fs:eax,4 //把立即数4放到fs段偏移为eax的地址
7 嵌入式汇编
参考:https://blog.csdn.net/yt_42370304/article/details/84982864
_asm_("asm statements":outputs:inputs:registers-modified);
举例
_asm_("pushl %%eax \n\t" "movl $0,%%eax \n\t" "popl %eax");
这段代码没有什么意义,纯粹是为了演示。而且省略了outputs、inputs等参数。有两个%%原因是编译嵌入式汇编语句的时候,会去掉一个%。
例子:
{register char _res;\ asm("push %%fs\n\t" "movw %%ax,%%fs\n\t" movb %%fs:%2,%%al\n\t" pop %%fs" :"=a"(_res):"0"(seg),"m"(*(addr)));\ _res;}
(注:在嵌入式汇编中,"a"、"b"、"c"、"d"分别表示eax、ebx、ecx和edx。详情见https://blog.csdn.net/yt_42370304/article/details/84982864)
在这个例子中首先定义一个寄存器变量_res
接着是一个嵌入式汇编语句,先看输出寄存器 "=a"(_res) ,=表示是一个输出寄存器,a表示eax,意思是,输出之后把eax中的内容赋给_res
输入寄存器: "0"(seg),"m"(*(addr)) ,seg和addr是前面定义的变量,代码里没表现出来。"0"表示和前面输出寄存器同一位置的寄存器,输出寄存器是eax,所以这里也使用eax。"0"(seg)表示把seg赋给eax,"m"表示一个内存地址,"m"(*(addr))表示把addr赋给一个内存地址。
输出寄存器和输入寄存器按照编号0-9排序的,所以"=a"(_res)序号是0,"0"(seg)是1,"m"(*(addr))是2。
我们再看主体,
push %%fs把fs压入堆栈
movw %%ax,%%fs把ax中的内容(即seg变量)放到fs变量中。
movb %%fs:%2,%%al //%2表示序号2也就是"m"(*(addr)),fs所指向的段,也就是addr所指向的地址取1字节放到al中
最后弹出fs
返回_res
例子:
int main(){ int a1=10,b1=0; _asm_("movl %1,%%eax;\\n\\r" "movl %%eax,%%ecx;" :"=a"(b1) :"b"(a1) :"%eax"); printf("Result:%d,%d\\n",a1,b1);}
8 伪指令
.开头的为伪指令
.section .data //表示数据段,可读写 //.data段定义的数据 .section .text //表示代码段,只读可执行 //.text段相关代码 .globl aaa //我们首先会把.s文件编译成.o文件。然后使用链接器链接这些.o文件,.globl aaa表示链接时,aaa对连接器可见
例子
movl $1, %eax movl $4, %ebx int $0x80
int为软中断,int $0x80,在linux内核中$0x80为系统调用,上面两条movl为传给该系统调用的参数。eax的值是系统调用号,1表示_exit系统调用,ebx的值则是传给_exit系统调用的参数,也就是退出状态。_exit这个系统调用会终止掉当前进程,而不会返回它继续执行
例子2
.section .data //声明数据段 data_items: .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0 //.long声明一组数,数组的首地址作为data_items符号所代表的地址 .section .text //声明代码段 .globl _start //声明globl标签_start,表示链接器可访问到 _start: movl $0, %edi //把立即数0放入edi,edi存放下标供循环中使用 movl data_items(,%edi,4), %eax //把edi*4+data_items的值放入eax,eax存放数组当前元素 movl %eax, %ebx //把eax放入ebx start_loop: //开始循环 cmpl $0, %eax //比较eax是否为0,为0则跳到loop_exit,(因为data_items最后一个元素为0,以此标识是否遍历完) je loop_exit //为0则跳到loop_exit incl %edi //edi增1 movl data_items(, %edi,4), %eax //把edi*4+data_items的值放入eax,eax存放数组当前元素 cmpl %ebx, %eax jle start_loop //jle也是一个条件跳转指令,le表示less than or equal movl %eax, %ebx jmp start_loop //jmp是一个无条件跳转指令 loop_exit: mov $1, %eax int $0x80 //触发中断,在linux内核中$0x80为系统调用,系统调用类型为eax中的值1(标识_exit系统调用),参数为eax2
(二)IO端口寻址
IO端口就是一个编号,通过IO端口我们可以读写IO设备。这些编号基本上是定死在内核代码中的。
IO端口的编址分为统一编址和独立编址。
统一编址的原理是把IO控制器的地址归入存储器寻址地址空间范围,因此这种编址方式也称为存储器映射编址,CPU访问IO控制器就像访问内存一样,使用的指令页一样。
IO端口独立编址是把IO控制器的寻址空间单独作为一个独立的地址空间对待。每个IO控制器端口有一个IO地址与之对应,并且使用专门的IO指控来访问端口。
1 IO端口地址分配
对于独立编址,其地址分配可以在/proc/ioports中查看。一般情况是:
(三)实模式和保护模式
实模式体现在内核中用到的地址都是真实物理地址。也就是说段基址:段内偏移地址产生的逻辑地址就是物理地址。也就是说程序员可见的地址就是真实的内存地址。
在内核启动时,开始阶段都是以实模式启动,然后再切换到保护模式。