内核代码绝大部分使用C语言编写,只有一小部分使用汇编语言编写,例如与特定体系结构相关的代码和对性能影响很大的代码。GCC提供了内嵌汇编的功能,可以在C代码中直接内嵌汇编语言语句,大大方便了程序设计。
一、基本内嵌汇编
GCC提供了很好的内嵌汇编支持,最基本的格式是:
__asm__ __volatile__(汇编语句模板);
1、__asm__
__asm__是GCC关键字asm的宏定义:
#define __asm__ asm
__asm__或asm用来声明一个内嵌汇编表达式,所以任何一个内嵌汇编表达式都是以它开头的,是必不可少的。
2、汇编语句模板
“汇编语句模板”是一组插入到C程序中的汇编指令(可以是单个指令,也可以是一组指令)。每条指令都应该由双引号括起,或者整组指令应该由双引号括起。每条指令还应该用一个定界符结尾。有效的定界符为换行符(\n)和分号(;)。\n后可以跟一个制表符(\t)作为格式化符号,增加GCC在汇编文件中生成的指令的可读性。
上述原则可以归结为:
①任意两个指令间要么被分号(;)分开,要么被放在两行;
②放在两行的方法既可以通过\n的方法来实现,也可以真正的放在两行;
③可以使用一对或多对双引号,每对双引号里可以放任意多条指令,所有的指令都必须放到双引号中。
在基本内嵌汇编中,“汇编语句模板”的书写的格式和你直接在汇编文件中使用汇编语言编程没有什么不同,你可以在其中定义标号(Label),定义对齐(.align n),定义段(.section name)。例如:
__asm__(".align 2\n\t"
"movl %eax, %ebx\n\t"
"test %ebx, %ecx\n\t"
"jne error\n\t"
"sti\n\t"
"error: popl %edi\n\t"
"subl %ecx, %ebx");
建议大家都使用这种格式来写内嵌汇编代码。
3、__volatile__
__volatile__是GCC关键字volatile的宏定义:
#define __volatile__ volatile
__volatile__或volatile是可选的。如果不想让GCC的优化改动你的内嵌汇编代码,你最好在前面都加上__volatile__。
二、带C语言表达式的内嵌汇编
在内嵌汇编中,可以将C语言表达式指定为汇编指令的操作数,而且不用去管如何将C语言表达式的值读入哪个寄存器,以及如何将计算结果写回C变量,你只要告诉程序中C语言表达式与汇编指令操作数之间的对应关系即可,GCC会自动插入代码完成必要的操作。
通常嵌入到C代码中的汇编语句很难做到与其它部分没有任何关系,因此更多时候需要用到扩展的内嵌汇编格式:
__asm__ __volatile__(汇编语句模板 : 输出部分 : 输入部分 : 破坏描述部分);
内嵌汇编表达式包含4个部分,各部分由“:”分隔。这4个部分都不是必须的,任何一个部分都可以为空,其规则为:
①如果“破坏描述部分”为空,则其
前面的“:”必须省略。比如:
__asm__("mov %%eax, %%ebx" : :);。
②如果“汇编语句模板”为空,则“输出部分”,“输入部分”以及“破坏描述部分”可以不为空,也可以为空。比如:
__asm__("" : : : "memory");。
③如果“输出部分”,“输入部分”以及“破坏描述部分”都为空,“输出部分”和“输入部分”之前的“:”既可以省略,也可以不省略。如果都省略,则此汇编退化为一个基本内嵌汇编,否则,仍然是一个带有C语言表达式的内嵌汇编。
④如果“输入部分”和“破坏描述部分”为空,但“输出部分”不为空,“输入部分”前的“:”既可以省略,也可以不省略。
⑤如果后面的部分不为空,而前面的部分为空,则前面的“:”都必须保留,否则无法说明不为空的部分究竟是第几部分。
⑥如果“破坏描述部分”不为空,而“输出部分”和“输入部分”都为空,则“输出部分”和“输入部分”前的“:”都必须保留。
从上面的规则可以看到另外一个事实,区分一个内嵌汇编是基本格式的还是扩展格式的,其规则在于在“汇编语句模板”后面是否有“:”的存在,如果没有则是基本格式的,否则,就是扩展格式的。
这两种格式对寄存器语法的要求不同:基本格式要求寄存器前只能使用一个%,这一点和原生汇编相同;而扩展格式则要求寄存器前必须使用两个%%。比如:
__asm__("mov %%eax, %%ebx" :)
__asm__("mov %eax, %ebx")
都是正确的写法,而
__asm__("mov %eax, %ebx" :)
__asm__("mov %%eax, %%ebx")
都是错误的写法。任何只带一个“%”的标识符都看成是操作数,而不是寄存器。
1、内嵌汇编举例
使用内嵌汇编,要先编写汇编语句模板,然后将C语言表达式与指令的操作数相关联,并告诉GCC对这些操作有哪些约束条件。例如在下面的汇编语句:
__asm__("movl %1, %0" : "=r"(result) : "m"(input));
“movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作数,称为占位符,内嵌汇编靠它们将C语言表达式与指令操作数相对应。指令模板后面用圆括号括起来的是C语言表达式,本例中只有两个:“result”和“input”,他们按照在输出部分和输入部分出现的顺序分别与指令操作数“%0”,“%1”对应;注意对应顺序:第一个C语言表达式对应“%0”;第二个表达式对应“%1”,依次类推。在每个操作数前面有一个用双引号括起来的字符串,字符串的内容是对该操作数的约束或者说要求。“result”前面的约束字符串是“=r”,其中“=”表示“result”在指令中是只写的(输出操作数),“r”表示需要将“result”与某个通用寄存器相关联,先将操作数的值读入寄存器,然后在指令
中使用相应寄存器,而不是“result”本身,当然指令执行完后需要将寄存器中的值存入变量“result”,从表面上看好像是指令直接对“result”进行操作,实际上GCC做了隐式处理,这样我们可以少写一些指令。“input”前面的“r”表示该表达式需要先放入某个寄存器,然后在指令中使用该寄存器参加运算。
由此可见,C语言表达式或者变量与寄存器的关系由GCC自动处理,我们只需使用约束字符串指导GCC如何处理即可。
内联汇编的重要性体现在它能够灵活操作,而且可以使其输出通过C变量显示出来。因为它具有这种能力,所以__asm__可以用作汇编指令和包含它的C程序之间的接口。
2、汇编语句模板
◆操作数
C语言表达式可用作内嵌汇编中的汇编指令的操作数。在汇编指令通过对C语言表达式进行操作来执行有意义的作业的情况下,操作数是扩展格式的内嵌汇编的主要特性。
每个操作数都由操作数约束字符串指定,后面跟着用圆括号括起来的C语言表达式,例如:
"constraint"(C expression)
操作数约束的主要功能是确定操作数的寻址方式。
◆占位符
在扩展格式的内嵌汇编的“汇编语句模板”中,操作数由占位符引用。如果总共有n个操作数(包括输入和输出),那么第一个输出操作数的编号为0,逐项递增,总操作数的数目限制在10个(%0、%1、…、%9)。
如果要处理很多输入和输出操作,数字型的占位符很快就会变得混乱。为了使条理清晰,GNU编译器(从版本3.1开始)允许声明替换的名称作为占位符。
替换的名称在“输入部分”和“输出部分”中声明。格式如下:
[name] "constraint"(C expression)
声明name后,使用%[name]的形式替换内嵌汇编代码中相应的数字型占位符。如下面所示:
__asm__("cmoveq %1, %2, %[result]"
: [result] "=r"(result)
: "r"(test), "r"(new), "[result]"(old));
在内嵌汇编中使用占位符表示的操作数,总被视为long型(4个字节) ,但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是高字节。方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1。
必须使用占位符的情况:
我们看一看下面这个例子:
__asm__("addl %1, %0"
:
"=a"(out)
: "m"(in1), "a"(in2));
①首先,我们看一看上例中的第1个输入操作表达式"m"(in1),它被GCC替换之后,表现为addl address_of_in1, %%eax,in1的地址是什么?编译时才知道。所以我们完全无法直接在指令中去写出in1的地址,这时使用占位符,交给GCC在编译时进行替代,就可以解决这个问题。所以这种情况
下,我们必须使用占位符。
②其次,如果上例中的输出操作表达式"=a"(out)改为"=r"(out),那么out究竟会使用哪个寄存器只有到编译时才能通过GCC来决定,既然在我们写代码的时候,我们不知道究竟哪个寄存器被选择,我们也就不能直接在指令中写出寄存器的名称,而只能通过占位符替代来解决。
3、输出部分
“输出部分”用来指定当前内嵌汇编语句的输出。我们看一看这个例子:
__asm__("movl %%cr0, %0" : "=a"(cr0));
这个内嵌汇编语句的输出部分为"=r"(cr0),它是一个“操作表达式”,更具体地在这里叫作“输出操作表达式”,指定了一个输出操作。“输出操作表达式”由两部分组成,这两部分都是必不可少的:
①圆括号括起来的部分是一个C语言表达式,用来保存内嵌汇编的一个输出值,其操作就等于C的赋值表达式cr0 = output_value,因此,圆括号中的输出表达式只能是C的左值表达式。那么右值output_value从何而来呢?
②答案是双引号中的内容,被称作“操作约束”(Operation Constraint),在这个例子中操作约束为"=a",它包含两个约束:等号(=)和字母a,其中等号(=)说明圆括号中左值表达式cr0是Write-Only的,只能够被作为当前内嵌汇编的输出,而不能作为输入。而字母a是寄存器EAX/AX/AL的简写,说明cr0的值要从EAX寄存器中获取,也就是说cr0 = %eax,最终这一点被转化成汇编语句就是movl %eax, address_of_cr0。
另外,需要特别说明的是,很多文档都声明,所有输出操作的操作约束必须包含一个等号(=),但GCC的文档中却很清楚的声明,并非如此。因为等号(=)约束说明当前的表达式是Write-Only的,但另外还有一个符号——加号(+)用来说明当前表达式是Read-Write的,如果一个操作约束中没有给出这两个符号中的任何一个,则说明当前表达式是Read-Only的。因为对于输出操作来说,肯定是必须是可写的,而等号(=)和加号(+)都表示可写,只不过加号(+) 同时也表示是可读的。所以对于一个输出操作来
说,其操作约束只需要有等号(=)或加号(+)中的任意一个就可以了。二者的区别是:等号(=)表示当前操作表达式指定了一个纯粹的输出操作,而加号(+)则表示当前操作表达式不仅仅只是一个输出操作还是一个输入操作。但无论是等号(=)约束还是加号(+)约束所约束的操作表达式都只能放在“输出部分”中,而不能被用在“输入部分”中。
在“输出部分”中可以有多个输出操作表达式,多个操作表达式中间必须用逗号(,)分开。
4、输入部分
“输入部分”的内容用来指定当前内嵌汇编语句的输入。我们看一看这个例子:
__asm__("movl %0, %%db7" : : "a"(cpu->db7));
例中“输入部分”的内
容为一个表达式"a"(cpu->db7),被称作“输入操作表达式”,用来表示一个对当前内嵌汇编的输入。
像输出操作表达式一样,一个输入操作表达式也分为两部分:带圆括号的部分(cpu->db7)和带双引号的部分"a"。这两部分对于一个内嵌汇编输入操作表达式来说也是必不可少的。
圆括号中的表达式cpu->db7是一个C语言的表达式,它不必是一个左值表达式,也就是说它不仅可以是放在C赋值操作左边的表达式,还可以是放在C赋值操作右边的表达式。所以它可以是一个变量,一个数字,还可以是一个复杂的表达式。比如上例可以改为:
__asm__("movl %0, %%db7" : : "a"(foo));
__asm__("movl %0, %%db7" : : "a"(0x1000));
__asm__("movl %0, %%db7" : : "a"(x*y/z));
双引号中的部分是约束部分,和输出操作表达式约束不同的是,它不允许指定加号(+)约束和等号(=)约束,也就是说它只能是默认的Read-Only的。约束中必须指定一个寄存器约束,例中的"a"表示当前输入变量cpu->db7要通过寄存器%eax输入到当前内嵌汇编中。
在“输入部分”中可以有多个输入操作表达式,多个操作表达式中间必须用逗号(,)分开。
5、操作约束
前面提到过,在内嵌汇编中的每个操作数都应该由操作数约束字符串描述,后面跟着用圆括号括起来的C语言表达式。操作数约束主要是确定指令中操作数的寻址方式。约束也可以指定:
汇编语言如何编程①是否允许操作数位于寄存器中,以及它可以包括在哪些类型的寄存器中
②操作数是否可以是内存引用,以及在这种情况下使用哪些类型的寻址方式
③操作数是否可以是立即数
约束字符必须与指令对操作数的要求相匹配,否则产生的汇编代码将会有错,在这个例子中:
__asm__("movl %1,%0" : "=r"(result) : "r"(input));
如果将那两个"r",都改为"m"(“m”表示操作数是内存引用)编译后得到的结果是:
movl input, result
很明显这是一条非法指令(mov不允许内存到内存的操作)。
每一个输入和输出操作表达式都必须指定自己的操作约束,下面是在80x86平台上可能使用的操作约束:
◆寄存器约束
当你当前的输入或输出需要借助一个寄存器时,你需要为其指定一个寄存器约束。你可以直接指定一个寄存器的名字,比如:
__asm__("movl %0, %%cr0" : : "eax"(cr0));
也可以指定一个缩写,比如:
__asm__("movl %0, %%cr0" : : "a"(cr0));
如果你指定一个缩写,比如“a”,则GCC将会根据当前操作表达式中C语言表达式的类型决定使用%eax,还是%ax或%al。比如:
unsigned short shrt;
__asm__("mov %0,%%bx" : : "a"(shrt));
由于变量shrt是16-bit short类型,则编译出来的汇编代码中,会让此变量使用%ax寄存器。
无论是输入还是输出的操

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。