Chinaunix首页 | 论坛 | 博客
  • 博客访问: 904431
  • 博文数量: 73
  • 博客积分: 2689
  • 博客等级: 少校
  • 技术积分: 897
  • 用 户 组: 普通用户
  • 注册时间: 2010-10-07 19:39
个人简介

一个有目标,为自己的未来努力奋斗的人

文章分类
文章存档

2015年(9)

2014年(2)

2013年(6)

2012年(11)

2011年(33)

2010年(12)

分类: C/C++

2010-12-02 22:04:02

内核代码绝大部分使用 C 语言编写,只有一小部分使用汇编语言编写,例如与特定体系结构相关的代码和对性能影响很大的代码。GCC 提供了内嵌汇编的功能,可以在 C 代码中直接内嵌汇编语言语句,大大方便了程序设计。简单的内嵌汇编很容易理解,例:

__asm__ __volatile__("hlt");
“__asm__”

表示后面的代码为内嵌汇编,“asm”是“__asm__”的别名。
“__volatile__” 表示编译器不要优化代码,后面的指令保留原样,“volatile”是它的别名。括号里面是汇编指令。

接上篇!

3.4 限制字符

3.4.1 限制字符列表

限制字符有很多种,有些是与特定体系结构相关,此处仅列出常用的限定字符和 i386中可能用到的一些常用的限定符。它们的作用是指示编译器如何处理其后的 C 语言变量与指令操作数之间的关系,例如是将变量放在寄存器中还是放在内存中等,下表列出了常用的限定字母。
  • 通用寄存器
“a”将输入变量放入 eax

这里有一个问题:假设 eax 已经被使用,那怎么办?其实很简单:因为 GCC 知道 eax已经被使用,
它在这段汇编代码的起始处插入一条语句 pushl %eax, eax 内容保存到堆栈,将然后在这段代码结束处再增加一条语句 popl %eax,恢复 eax 的内容。

“b”将输入变量放入 ebx
“c”将输入变量放入 ecx
“d”将输入变量放入 edx
“s”将输入变量放入 esi
“d”将输入变量放入 edi
“q”将输入变量放入 eax,ebx ,ecx ,edx 中的一个
“r”将输入变量放入通用寄存器,也就是 eax ,ebx,ecx,edx,esi,edi 中的一个
“A”把 eax 和 edx,合成一个 64 位的寄存器(uselong longs)
“m”内存变量
“o”操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加
变址寻址
“V”操作数为内存变量,但寻址方式不是偏移量类型
“,” 操作数为内存变量,但寻址方式为自动增量
“p”操作数是一个合法的内存地址(指针)
  • 寄存器或内存
“g” 将输入变量放入 eax,ebx,ecx ,edx 中的一个或者作为内存变量
“X”操作数可以是任何类型立即数
“I” 0-31 之间的立即数(用于 32 位移位指令)
“J” 0-63 之间的立即数(用于 64 位移位指令)
“N” 0-255 ,之间的立即数(用于 out 指令)
“i” 立即数
“n” 立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i”
  • 匹配
“0”“1”...“9 ”表示用它限制的操作数与某个指定的操作数匹配,也即该操作数就是指定的那个操作数,例如用“0”去描述“%1”操作数,那么“%1”引用的其实就是“%0”操作数,注意作为限定符字母的 0-9 ,与指令中的“%0”-“%9”的区别,前者描述操作数,后者代表操作数。后面有详细描述。

& 该输出操作数不能使用过和输入操作数相同的寄存器,后面有详细描述。
  • 操作数类型
“=” 操作数在指令中是只写的(输出操作数)
“+” 操作数在指令中是读写类型的(输入输出操作数)
  • 浮点数
“f”
  • 浮点寄存器
“t”第一个浮点寄存器
“u”第二个浮点寄存器
“G”标准的 80387
  • 浮点常数
% 该操作数可以和下一个操作数交换位置
例如 addl 的两个操作数可以交换顺序(当然两个操作数都不能是立即数)
# 部分注释,从该字符到其后的逗号之间所有字母被忽略
* 表示如果选用寄存器,则其后的字母被忽略
现在继续看上面的例子,"=m" (ADDR)表示 ADDR 为内存变量(“m”),而且是输出变量(“=”);"Ir" (nr)表示 nr 为 0-31 之间的立即数(“I”)或者一个寄存器操作数(“r”)。

3.4.2 匹配限制符

I386 指令集中许多指令的操作数是读写型的(读写型操作数指先读取原来的值然后参加运算,最后将结果写回操作数),例如 addl %1,%0,它的作用是将操作数%0 与操作数%1的和存入操作数%0,因此操作数%0 是读写型操作数。老版本的 GCC 对这种类型操作数的支持不是很好,它将操作数严格分为输入和输出两种,分别放在输入部分和输出部分,而没有一个单独部分描述读写型操作数,因此在 GCC 中读写型的操作数需要在输入和输出部分分别描述,靠匹配限制符将两者关联到一起。注意仅在输入和输出部分使用相同的 C 变量,但是不用匹配限制符,产生的代码很可能不对,后面会分析原因。

匹配限制符是一位数字: “0”“1”......“9”,分别表示它限制的 C 表达式分别与占位符%0,%1,......%9 对应的 C 变量匹配。例如使用“0”作为%1 的限制字符,那么%0 和%1 表示同一个 C 变量。

看一下下面的代码就知道为什么要将读写型操作数,分别在输入和输出部分加以描述。

该例功能是求 input+result 的和,然后存入 result:

extern int input,result;
void test_at_t()
{
result= 0;
input = 1;
__asm__ __volatile__ ("addl %1,%0":"=r"(result): "r"(input));
}

对应的汇编代码为:

movl $0,_result
movl $1,_input
movl _input,%edx
#APP
addl %edx,%eax
#NO_APP
movl %eax,%edx
movl %edx,_result

input 为输入型变量,而且需要放在寄存器中,GCC 给它分配的寄存器是%edx,在执行 addl 之前%edx 的内容已经是 input 的值。可见对于使用“r”限制的输入型变量或者表达式,在使用之前 GCC 会插入必要的代码将他们的值读到寄存器;“m”型变量则不需要这一步。读入 input 后执行 addl,显然%eax 的值不对,需要先读入 result 的值才行。再往后看:

movl %eax,%edx 和 movl %edx,_result 的作用是将结果存回 result,分配给 result 的寄存器与
分配给 input 的一样,都是%edx。

综上可以总结出如下几点:
[1] 使用“r”限制的输入变量,GCC 先分配一个寄存器,然后将值读入寄存器,最后用该寄存器替换占位符;
[2] 使用“r”限制的输出变量,GCC 会分配一个寄存器,然后用该寄存器替换占位符,但是在使用该寄存器之前并不将变量值先读入寄存器,GCC 认为所有输出变量以前的值都没有用处,不读入寄存器(可能是因为 AT&T 汇编源于 CISC 架构处理器的汇编语言,在 CISC 处理器中大部分指令的输入输出明显分开,而不像 RISC
那样一个操作数既做输入又做输出,例如 add r0,r1,r2,r0和 r1 是输入, r2是输出,输入和输出分开,没有使用输入输出型操作数,这样我们就可以认为 r2 对应的操作数原来的值没有用处,也就没有必要先将操作数的值读入 r2,因为这是浪费处理器的 CPU 周期),最后 GCC 插入代码,将寄存器的值写回变量;
[3] 输入变量使用的寄存器在最后一处使用它的指令之后,就可以挪做其他用处,因为已经不再使用。例如上例中的%edx。在执行完 addl 之后就作为与 result 对应的寄存器。

因为第二条,上面的内嵌汇编指令不能奏效,因此需要在执行 addl 之前把 result 的值读入寄存器,也许再将 result 放入输入部分就可以了(因为第一条会保证将 result 先读入寄存器)。修改后的指令如下(为了更容易说明问题将 input 限制符由“r,”改为“m”):

extern int input,result;
void test_at_t()
{
result = 0;
input = 1;
__asm__ __volatile__ ("addl %2, %0":"=r"(result):"r"(result),"m"(input));
}

看上去上面的代码可以正常工作,因为我们知道%0 和%1 都和 result 相关,应该使用同一个寄存器,但是 GCC 并不去判断%0 和%1,是否和同一个 C 表达式或变量相关联(这样易于产生与内嵌汇编相应的汇编代码)
,因此%0 和%1 使用的寄存器可能不同。我们看一下汇编代码就知道了。

movl $0,_result
movl $1,_input
movl _result,%edx
#APP
addl _input,%eax
#NO_APP
movl %eax,%edx
movl %edx,_result

现在在执行 addl 之前将 result 的值被读入了寄存器%edx,但是 addl 指令的操作数%0却成了%eax,而不是%edx,与预料的不同,这是因为 GCC 给输出和输入部分的变量分配了不同的寄存器,GCC 没有去判断两者是否都与 result 相关,后面会讲 GCC 如何翻译内嵌汇编,看完之后就不会惊奇啦。

使用匹配限制符后,GCC 知道应将对应的操作数放在同一个位置(同一个寄存器或者同一个内存变量)。使用匹配限制字符的代码如下:

extern int input,result;
void test_at_t()
{
result = 0;
input = 1;
__asm__ __volatile__ ("addl %2,%0":"=r"(result):"0"(result),"m"(input));
}

输入部分中的 result 用匹配限制符“0”限制,表示%1 与%0 代表同一个变量,输入部分说明该变量的输入功能,输出部分说明该变量的输出功能,两者结合表示 result 是读写型。因为%0 和%1 表示同一个 C 变量,所以放在相同的位置,无论是寄存器还是内存。相应的汇编代码为:

movl $0,_result
movl $1,_input
movl _result,%edx
movl %edx,%eax /APP
addl _input,%eax /NO_APP
movl %eax,%edx
movl %edx,_result

可以看到与 result 相关的寄存器是%edx,在执行指令 addl 之前先从%edx 将 result 读入%eax,执行之后需要将结果从%eax 读入%edx,最后存入 result 中。这里我们可以看出 GCC处理内嵌汇编中输出操作数的一点点信息 :addl 并没有使用%edx,可见它不是简单的用result 对应的寄存器%edx 去替换%0,而是先分配一个寄存器,执行运算,最后才将运算结果存入对应的变量,因此 GCC 是先看该占位符对应的变量的限制符,发现是一个输出型寄存器变量,就为它分配一个寄存器,此时没有去管对应的 C 变量,最后 GCC 知道还要将寄
存器的值写回变量,与此同时,它发现该变量与%edx 关联,因此先存入%edx,再存入变量。至此读者应该明白了匹配限制符的意义和用法。在新版本的 GCC 中增加了一个限制字符“+”,它表示操作数是读写型的,GCC 知道应将变量值先读入寄存器,然后计算,最后写回变量,而无需在输入部分再去描述该变量。例:

extern int input,result;
void test_at_t()
{
result = 0;
input = 1;
__asm__ __volatile__ ("addl %1,%0":"+r"(result):"m"(input));
}

此处用“+”替换了“=”,而且去掉了输入部分关于 result 的描述,产生的汇编代码如下:

movl $0,_result
movl $1,_input
movl _result,%eax /APP
addl _input,%eax /NO_APP
movl %eax,_result
L2:
movl
%ebp,%esp

处理的比使用匹配限制符的情况还要好,省去了好几条汇编代码。

3.4.3 “&”限制符

限制符“&”在内核中使用的比较多,它表示输入和输出操作数不能使用相同的寄存器,这样可以避免很多错误。

举一个例子,下面代码的作用是将函数 foo 的返回值存入变量 ret 中:

__asm__ ( “call foo;movl %%edx,%1”, :”=a”(ret) : ”r”(bar) );

我们知道函数的 int 型返回值存放在%eax 中,但是 gcc 编译的结果是输入和输出同时使用了寄存器%eax,如下:

movl bar, %eax
#APP
call foo
movl %edx,%eax
#NO_APP
movl %eax, ret

结果显然不对,原因是 GCC 并不知道%eax 中的值是我们所要的。避免这种情况的方法是使用“&”限定符,这样 bar 就不会再使用%eax 寄存器,因为已被 ret 指定使用。

_asm__ ( “call foo;movl %%edx,%1”,:”=&a”(ret) : ”r”(bar) );

3.5 破坏描述部分

接下篇...



阅读(1008) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

chinaunix网友2010-12-03 15:51:45

很好的, 收藏了 推荐一个博客,提供很多免费软件编程电子书下载: http://free-ebooks.appspot.com