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 ...
随机推荐
- Elasticsearch(ES)的高级搜索(DSL搜索)(下篇)
1. 概述 之前聊了Elasticsearch(ES)的高级搜索(DSL搜索)的一部分内容,今天把剩下的部分聊完. 2. 场景说明 2.1 创建索引同时创建映射 PUT http://192.168 ...
- Linux系列(32) - rpm命令管理之RPM查询(4)
RPM包默认安装位置 RPM包默认安装路径 /etc/ 配置文件安装目录 /usr/bin/ 可执行的命令安装目录 /usr/lib/ 程序所使用的函数库保存位置 /usr/share/doc/ 基本 ...
- CF346E-Doodle Jump【类欧】
正题 题目链接:https://www.luogu.com.cn/problem/CF346E 题目大意 给出\(a,n,p,h\),在每个\(ax\%p(x\in[0,n])\)的位置有一个关键点, ...
- P4585-[FJOI2015]火星商店问题【线段树,可持久化Trie】
正题 题目链接:https://www.luogu.com.cn/problem/P4585 题目大意 \(n\)个集合,开始每个集合中有一个数字. 开启新的一天并且往集合\(s\)中插入数字\(v\ ...
- Python日常Bug集
1.TypeError: 'int' object is not iterable: 场景示例: data = 7 for i in data: print(i) # 原因:直接对int数据进行迭代造 ...
- oracle 查看表结构语句
desc + 表名 describe命令 列出指定表的列定义,视图或同义词,或指定函数或存储过程的详述. 语法:desc[ribe] {[模式.]对象[@链接串]} 模式 表示对象驻留的架构.如果 ...
- 【Go】Golang实现gRPC的Proxy的原理
背景 gRPC是Google开始的一个RPC服务框架, 是英文全名为Google Remote Procedure Call的简称. 广泛的应用在有RPC场景的业务系统中,一些架构中将gRPC请求都经 ...
- PHP的SPL扩展库(一)数据结构
SPL 库也叫做 PHP 标准库,主要就是用于解决典型问题的一组接口或类的集合.这些典型问题包括什么呢?比如我们今天要讲的数据结构,还有一些设计模式的实现,就像我们之前讲过的观察者模式相关的接口在 S ...
- Django+Nginx+Uwsgi(全网最全步骤工作原理流程与部署历程)
一.必要前提 1.1 准备知识 django 一个基于python的开源web框架,请确保自己熟悉它的框架目录结构. uWSGI 一个基于自有的uwsgi协议.wsgi协议和http服务协议的web网 ...
- ES5新增方法--查找方法--forEach(),filter(),some()区别
1.forEach方法 迭代(遍历)数组 var arr = [1, 2, 3]; var sum = 0; arr.forEach(function (value, index, array) { ...