linux 内核源代码情景分析——linux 内核源码中的汇编语言代码
1. 用汇编语言编写部分核心代码的原因:
① 操作系统内核中的底层程序直接与硬件打交道,需要用到一些专用的指令,而这些指令在C语言中并无对应的语言成分;
② CPU中的一些特殊指令也没有对应的C语言成分,如关中断、开中断等等;
③ 内核中的某些函数在运行时会非常频繁的被调用,因此效率就显得很重要,用汇编语言写的程序效率通常要比高级语言编写的高;
④ 在某些特殊场合,一段程序的空间效率也会显得很重要;
2. linux 采用了AT&T的386汇编语言格式,而没有用Intel的,它们之间的差别主要有:
① 在Intel格式中大多使用大写字母,而在AT&T格式中都使用小写字母;
② 在AT&T格式中,寄存器名要加上“%”作为前缀,而在Intel格式中则不带前缀;
③ 在AT&T的386汇编语言中,指令的源操作数与目标操作数的顺序与在Intel的386汇编语言中正好相反;在Intel格式中是目标在前,源在后;而在AT&T格式中则是源在前,目标在后;
④ 在AT&T格式中,访内指令的操作数大小由操作码名称的最后一个字母来决定,用作操作码后缀的字母有b(表示8位),w(表示16位)和l(表示32位)。而在Intel格式中,则是在表示内存单元的操作数前面加上“BYTE PTR”,“WORD PTR”,或“DWORD PTR”来表示;
⑤ 在AT&T格式中,直接操作数要加上“$”作为前缀,而Intel则不用带前缀;
⑥ 在AT&T格式中,绝对转移或调用指令jump/call 的操作数(也即转移或调用的目的地址),要加上“*”作为前缀,而在Intel中则不带;
⑦ 远程的转移指令和子程序调用指令的操作码名称,在AT&T格式中位“ljmp”和“lcall”,而在Intel格式中,则为“JMP FAR”和“CALL FAR”。当转移和调用的目标位直接操作数时,两种不同的表示如下:
CALL FAR SECTION:OFFSET(Intel 格式)
JMP FAR SECTION:OFFSET(Intel 格式)
lcall $section, $offset (AT&T 格式)
ljmp $section, $offset (AT&T格式)
⑧ 间接寻址的一般格式,两者区别如下:
SECTION:[BASE+INDEX*SCALE+DISP] (Intel 格式)
section: disp(base, index, scale) (AT&T 格式)
3. 嵌入C代码中的386汇编语言程序段
当需要在C语言的程序中嵌入一段汇编语言程序段时,可以使用gcc提供的“asm”语句功能,例如:
#define __SLOW_DOWN_IO __asm__ __volatile__ ("outb %al, $0x80")
这里暂且忽略在asm和volatile前后的两个“__”字符,这也是gcc对C语言的一种扩充
在同一个asm语句中可以插入多行汇编程序,例如:
#define __SLOW_DOWN_IO __asm__ __volatile__ ("jmp 1f \n1:\tjmp 1f \n1:")
这里,一共插入了3行汇编语句,“\n”是换行符,而“\t”则表示TAB符,所以gcc将之翻译成下面的格式而交给gas去汇编:
jmp 1f
1: jmp1f
1:
这里转移指令的目标1f表示往前找到第一个标号为1的那一行。相应地,如果1b就表示往后找。所以,这一小段汇编代码的用意就是使CPU空做两条转移指令而消耗掉一些时间。既然是要消耗掉一些时间,而不是要节省一些时间,那么为什么要用汇编语句来实现,而不是在C里面来实现呢?原因在于想要对此有比较确切的控制。如果用C语句来消耗一些时间的话,你常常不能确定地知道经过编译以后,特别是如果经过优化的话,最后产生的汇编代码究竟怎样。
接着看例子:
static __inline__ void atomic_add(int i, atomict_t *v)
{
__asm__ __volatile__ (
LOCK "addl %1, %0"
: "=m" (v->counter)
: "ir" (i), "m" (v->counter));
}
下面,先介绍一下插入C代码中的汇编成分的一般格式
插入C代码中的一个汇编语言代码片段可以分成四部分,以“:”号加以分隔,其一般形式为:
指令部:输出部:输入部:损坏部
第一部分就是汇编语句本身,其格式与在汇编语言程序中使用的基本相同,但也有区别,后面会讲到不同之处。
指令不是必须得有的,其他各部分则可视具体的情况而省略,在最简单的情况下就与常规的汇编语句基本相同,如前面的两个例子那样。
当将汇编语言代码片段嵌入到C代码中时,操作数与C代码中的变量如何结合显然是个问题,gcc采取了办法是:程序员只提供具体的指令,而对寄存器的使用则一般只提供一个“样板”和一些约束条件,而把到底如何与变量结合的问题留给gcc和gas去处理。
在指令部中,数字加上前缀%,如%0,%1等等,表示需要使用寄存器的样板操作数,可以使用的此类操作数的总数取决于具体CPU中通用寄存器的数量。这样,指令部中用到了几个不同的这种操作数,就说明有几个变量需要与寄存器结合,由gcc和gas在编译和汇编时根据后面的约束条件自行变通处理。由于这些样板操作数也使用“%”前缀,在涉及到具体的寄存器时就要在寄存器名前面加上两个“%”符,以免混淆。
那么,怎样表达对变量的结合的约束条件呢?这就是其余几个部分的作用。紧接在指令部后面的是“输出部”,用于规定对输出变量,即目标操作数如何结合的约束条件。每个这样的条件称为一个“约束”,必要时输出部中可以有多个约束,互相以逗号隔开,每个输出约束以“=”号开头,然后是一个字母表示对操作数类型的说明,然后是关于变量结合的约束。例如:上面例子中,输出部为
: "=m" (v->counter)
这里只有一个约束,“=m”表示相应的目标操作数(指令部中的%0)是一个内存单元v->counter。凡是与输出部说明的操作数相结合的寄存器或操作数本身,在执行嵌入的汇编代码以后均不保留执行之前的内容,这就给gcc提供了调度使用这些寄存器的依据。
输出部后面就是“输入部”,输入约束的格式和输出约束相似,但不带“=”号。前面例子中的输入部有两个,第一个为“ir”(i),表示指令中的%1可以是一个在寄存器中的“直接操作数”,第二个约束为“m”(v->counter),意义与输出约束相同,如果一个输入约束要求使用寄存器,则在预处理时gcc会为之分配一个寄存器,并自动插入必要的指令将操作数即变量的值装入该寄存器。与输入部中说明的操作数结合的寄存器或操作数本身,在执行嵌入的汇编代码以后也不保留执行之前的内容。例如,这里的%1要求使用寄存器,所以gcc为其分配一个寄存器,并自动插入一条movl指令把参数i的数值装入该寄存器,可是这个寄存器原来的内容就不复存在了。
操作数的编号从输出部的第一个约束(序号为0)开始,顺序数下来,每个约束计数一次,表示约束条件的字母很多,主要有:
“m”,“v”,“o” ——表示内存单元;
“r” ——表示任何寄存器;
“q” ——表示eax,ebx,ecs,edx 之一;
“i”和“h” ——表示直接操作数;
“E”和“F” ——表示浮点数;
“g” ——表示“任意”;
“a”,“b”,“c”,“d” ——表示使用寄存器eax,ebx,ecs,edx
“S”和“D” ——表示要求使用寄存器esi和edi
“I” ——表示常数(0至31)
看一个例子就明白了,例:
static inline void * __memcpy(void *to, const void *from, size_t)
{
int d0, d1, d2;
__asm__ __volatile__(
"rep; movsl\n\t"
"testb $2, %b4\n\t"
"je 1f\n\t"
"movsw\n"
"1:\ttestb $1, %b4\n\t"
"je 2f\n\t"
"movsb\n"
"2:"
: "=&c" (d0), "=&D" (d1) , "=&S" (d2)/*输出部*/
: "0" (n/4), "q" (n), "1" ((long) to), "2" ((long) from)/*输入部*/
: "memory");
return (to);
}
输出部有3个约束,变量d0为操作数%0,必须放在寄存器ecx中,d1即%1必须放在寄存器edi中,d2即%2放在esi中;
输入部有4个约束,对应于操作数%3到6%,其中操作数%3与操作数%0使用同一个寄存器即ecx,并且要求由gcc自动插入必要的指令,事先将其设置成n/4,这里的作用是将复制长度从字节个数n换算成长字个数n/4.至于n本身,则要求gcc分配任意一个寄存器存放,对应于操作数%4.操作数5%和6%,即参数to和from,分别与%1和%2使用相同的寄存器,所以,必须是edi和esi。
因为我搞不懂,所以下面解释一下这段代码的含义:
第一条指令是“rep”,表示下一条指令movsl要重复执行,每重复一遍就要把寄存器ecx的内容减1,知道变为0为止。因为在执行指令之前,ecx被放进了n/4,所以,movsl指令执行了n/4次,那么movsl又干些什么呢?它从esi所指的地方复制一个长字到edi所指的地方,并使esi和edi分别加4.这样,当执行完第5行后,所有的长字都已复制好,最多只剩下3个字节了。
接着就是处理剩下的字节了,先通过testb测试操作数%4即复制长度n的最低字节中的bit2(对操作数进行的字节操作默认为对其最低字节操作,也可以明确指出是对哪一个字节操作,在%与序号之间插入b表示最低字节,插入h表示次低字节,$2表示立即数),如果这一位为1,说明还有至少2个字节,所以通过指令movw复制1个短字(esi和edi分别加2),否则就把它跳过。(testb是做AND运算,但不会把结果写回目的操作数,仅根据结果的值来影响标志位,je当EFLAGS的ZF标志为1时才跳转,ZF为1说明上一次运算结果为0)再通过testb测试操作数%4的bit1,如果为1说明还剩下一个字节,所以通过指令movsb再复制一个字节,否则就把它跳过。到达标号2的时候,执行就结束了。
linux 内核源代码情景分析——linux 内核源码中的汇编语言代码的更多相关文章
- linux 内核源代码情景分析——linux 内存管理的基本框架
386 CPU中的页式存管的基本思路是:通过页面目录和页面表分两个层次实现从线性地址到物理地址的映射.这种映射模式在大多数情况下可以节省页面表所占用的空间.因为大多数进程不会用到整个虚存空间,在虚存空 ...
- linux 内核源代码情景分析——linux 内核源代码中的C语言代码
linux 内核的主体是以GNU的C语言编写的,GNU为此提供了编译工具gcc.GNU对C语言本身作了不少扩充. 1) gcc 从 C++ 语言中吸收了"inline"和" ...
- Linux内核源代码情景分析系列
http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统 5.1 概述 构成一个操作系统最重要的就 ...
- 在Android源码中查找Java代码中native函数对应的C++实现
Android源码中很多关键代码都是C++实现的,java通过jni来调用,经常会看到java中这样的代码: static native Thread currentThread(); 如何根据方法名 ...
- Linux内核源代码情景分析-fork()
父进程fork子进程: child = fork() fork经过系统调用.来到了sys_fork.具体过程请參考Linux内核源码情景分析-系统调用. asmlinkage int sys_fork ...
- linux 内核源代码情景分析——地址映射的全过程
linux 内核采用页式存储管理.虚拟地址空间划分成固定大小的"页面",由MMU在运行时将虚拟地址映射成某个物理内存页面中的地址.页式内存管理比段式内存管理有很多好处,但是由于In ...
- jQuery1.11源码分析(2)-----Sizzle源码中的正则表达式[原创]
看完了上篇,对Sizzle有了一个大致的了解,我们接下来就可以正式开始啃Sizzle的源码了.上来就讲matcher难度太大,先来点开胃菜,讲讲Sizzle中的各个正则表达式的作用吧(本来还想讲初始化 ...
- linux 内核源代码情景分析——用户堆栈的扩展
上一节中,我们浏览了一次因越界访问而造成映射失败从而引起进程流产的过程,不过有时候,越界访问时正常的.现在我们就来看看当用户堆栈过小,但是因越界访问而"因祸得福"得以伸展的情景. ...
- Linux内核源代码情景分析-中断半
一.中断初始化 1.中断向量表IDT初始化 void __init init_IRQ(void) { int i; #ifndef CONFIG_X86_VISWS_APIC init_ISA_irq ...
随机推荐
- 洛谷P1449——后缀表达式(栈模拟)
题目描述 所谓后缀表达式是指这样的一个表达式:式中不再引用括号,运算符号放在两个运算对象之后,所有计算按运算符号出现的顺序,严格地由左而右新进行(不用考虑运算符的优先级). 如:3*(5–2)+7对应 ...
- 华为云计算IE面试笔记-桌面云用户登录连接流程及故障处理?
1-10:桌面与系统验证成功 http协议 11-19:桌面list(VM列表)获取,选择 http协议 20-30: ...
- 深入HTML5第二天
sub(subscripted下标标签)和sup(superscripted上标标签) 内联元素:inline element span(范围标签):内联元素inline element 特性:没有 ...
- git pull 时remote: HTTP Basic: Access denied解决方案
当qian windows用户密码过期更改了密码后,操作git pull 拉取远程仓库代码或git push时报错 如下:remote: HTTP Basic: Access denied Auth ...
- Gitee自动化部署python脚本
一.前期准备 1.1 安装环境 1.安装python3 2.打开命令行安装selenium pip install selenium 二.python代码 2.1 源码 #!/usr/bin/pyth ...
- php页面 数组根据下标来排序
$a = [ ['id'=>1,'title'=>'星期二的早晨','author'=>'张三','date'=>'2021-6-1'], ['id'=>2,'title ...
- 15种Python片段去优化你的数据科学管道
来源:15 Python Snippets to Optimize your Data Science Pipeline 翻译:RankFan 15种Python片段去优化你的数据科学管道 为什么片段 ...
- 关于VS中的无法解析的外部符号问题
利用caffe的源码编译出的caffe.lib静态链接库里面就包含了源码里面的那些函数的接口i,所以如果在程序中使用的是源码的话,就不需要在链接器里面再添加此静态链接库了 对于无法解析的外部符号,首先 ...
- bzoj1858SCOI 序列操作 (线段树)
题目大意: 给定一个长度为n的01序列为,现在有m种操作 \(0\ a\ b\) 把\([a,b]\)的数全部修改为0 \(1\ a\ b\) 把\([a,b]\)的数全部修改为1 \(2\ a\ b ...
- 小数的十进制和二进数转换 “无限不循环”小数的IEEE 754表示
十进制 -> 二进制 将整数部分和小数部分分开处理 例:3.125(10) 其整数部分为11(2) 小数部分按照下面的步骤求解: 0.125 x 2 = 0.25 取0 0.250 x 2 = ...