gnu嵌入汇编,内嵌汇编详细的介绍

GCC 提供了内嵌汇编的功能,可以在 C 代码中直接内嵌汇编语言语句,大大方便了程序设计。简单的内嵌汇编很容易理解,例:

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

我们的目的是要理解这两条语句

    1. asm volatile ("inb %1, %0" : "=a" (data) : "d" (port) : "memory");
    2.     asm volatile (
        "cld;"
        "repne; insl;"
        : "=D" (addr), "=c" (cnt)
        : "d" (port), "0" (addr), "1" (cnt)
        : "memory", "cc");

内嵌汇编有一个语法模板

__asm__(
汇编语句模板:
输出部分:
输入部分:
破坏描述部分)

下面是一个具体的实际例子

__asm__ __volatile__(
"cli":	------>这个是汇编指令部分
:		------->这个是输出部分,为空
:		-------->这个是输入部分,为空
"memory"	-------->这个是破坏描述部分
)

下面按照顺序来依次进行讲解每一个部分大概的功能。
备注: 汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”格开,相应部分内容为空。例如:

__asm__ __volatile__(
"cli":
:
:"memory")

汇编语句模板

汇编语句模板由汇编语句序列组成,语句之间使用“;”、“\n”或“\n\t”分开
我们可以使用一个例子看一下这几个分隔符的区别?

int main()
{
	__asm__ __volatile__ (
		"cld\n\t"
		"cld\n\t"
	);
	return 0;
}

查看一下预处理阶段又没有处理

GCC编程四个过程:预处理-编译-汇编-链接
http://hi.baidu.com/hp_roc/blog/item/91691146c40de946500ffe39.html
下面是预处理的结果

sgy@ubuntu:~/sgy/user_program/test$ gcc -E test.c
# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "test.c"

int main()
{
 __asm__ __volatile__ (
  "cld\n\t"
  "cld\n\t"
 );
 return 0;
}
sgy@ubuntu:~/sgy/user_program/test$ 

我们发现预处理阶段其实并没有处理,我们看一下编译阶段干了什么

sgy@ubuntu:~/sgy/user_program/test$ cat test.s 
	.file	"test.c"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
#APP
# 4 "test.c" 1
	cld
	cld
	
# 0 "" 2
#NO_APP
	movl	$0, %eax
	popl	%ebp
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
	.section	.note.GNU-stack,"",@progbits
sgy@ubuntu:~/sgy/user_program/test$ 

我们发现嵌入汇编真正处理的阶段是在编译阶段做的, 我们把test.c 更改一下,把"\n\t" 替换成 ";"号

int main()
{
	__asm__ __volatile__ (
		"cld;"
		"cld;"
	);
	return 0;
}

看一下最终的编译结果

#APP
# 4 "test.c" 1
	cld;cld;
# 0 "" 2
#NO_APP

用gcc直接编译成可执行文件也不会出错

sgy@ubuntu:~/sgy/user_program/test$ gcc  test.c
sgy@ubuntu:~/sgy/user_program/test$ 

我们再把test.c 里面的";" 替换成 “\n”

#APP
# 4 "test.c" 1
	cld
cld
# 0 "" 2
#NO_APP

这三种结果只是在排版上面会出现不一样,其实并不影响结果的编译和运行。
但是明显"\n\t" 效果最好,因为排版最好看。

asm volatile ("inb %1, %0" : "=a" (data) : "d" (port) : "memory");

ok,这条语句的第一部分是汇编指令部分,且只有一条汇编指令,我们已经掌握了。那么 %1, %0是什么意思?

0,1 数字代表依次从 输出部分开始的变量的编号,这个是对应的, 例如%0表示的就是data变量, %1代表的是port变量

 "=a" (data)  这个是什么意思呢?

等于号:表示其是一个输出操作数,例如赋值给data,或更改data这个变量的值

每个输出操作数的限定字符串必须包含“=”表示它是一个输出操作数

后面紧跟着的a是限定字符, 表示输出操作数要放入到eax寄存器里面

其他的各个字符所代表的意思
“b”将输入变量放入 ebx
“c”将输入变量放入 ecx
“d”将输入变量放入 edx

即port变量需要和 edx寄存器绑定起来
最后面的那个memory是个什么意思。

一些需要的背景知识
编译器会对代码进行优化,以提高代码的运行效率和速度(他们一种优化的方式是将指令乱序执行, 另外一种方式是使用缓存),但是这些编译优化毕竟不是万能的,某些硬件设备要求一部分指令按照特定的顺序执行,所以不能优化他们。

linux 提供了一个宏解决编译器的执行顺序问题。

void Barrier(void)

这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前 CPU寄存器中的所有修改过的数值存入内存,需要这些数据的时候再重新从内存中读出。所以就不会使用到缓存里面的内容。

基于上面的这些介绍,memory的功能主要如下
  (1)不要将该段内嵌汇编指令与前面的指令重新排序;也就是在执行内嵌汇编代码之前,它前面的指令都执行完毕
  (2)不要将变量缓存到寄存器,因为这段代码可能会用到内存变量,而这些内存变量会以不可预知的方式发生改变,因此 GCC 插入必要的代码先将缓存到寄存器的变量值写回内存,如果后面又访问这些变量,需要重新访问内存。

我们可以把上面那一条指令通过反汇编来进行查看,看一下翻译成汇编究竟长什么样子,稍微修改了一下原先代码的样子

int add(int port) {
    int data;
    asm volatile ("add %1, %0" : "=a" (data) : "d" (port) : "memory");
    return data;
}
int main()
{
	add(80);
	return 0;
}

我们可以看一下add函数的汇编代码是什么样子的。

(gdb) si
4	    asm volatile ("add %1, %0" : "=a" (data) : "d" (port) : "memory");
1: x/6i $pc
=> 0x80483f3 <add+6>:	mov    0x8(%ebp),%eax
   0x80483f6 <add+9>:	mov    %eax,%edx
   0x80483f8 <add+11>:	add    %edx,%eax
   0x80483fa <add+13>:	mov    %eax,-0x4(%ebp)
   0x80483fd <add+16>:	mov    -0x4(%ebp),%eax
   0x8048400 <add+19>:	leave  
(gdb) 

我们查看一下port和data变量的地址

(gdb) p /x &port 
$1 = 0xbffff0d4
(gdb) p /x &data
$2 = 0xbffff0c8

这两条指令是用来取出port变量的

=> 0x80483f3 <add+6>:	mov    0x8(%ebp),%eax
   0x80483f6 <add+9>:	mov    %eax,%edx

ebp的值是多少

(gdb) info reg ebp
ebp            0xbffff0cc	0xbffff0cc
(gdb) 

ebp + 8 = 0xbffff0d4 刚好是port变量的地址, 一开始是将port变量放到eax寄存器里面,但是发现与port变量绑定的是edx寄存器,所以又把edx寄存器放到了eax寄存器里面,
最后的那一条add指令,说明data变量确实是与eax寄存器绑定的。

 0x80483f8 <add+11>:	add    %edx,%eax

我们可以把上面的a,d, 改成b,c尝试一下。data变量和ebx绑定,port和ecx绑定

    asm volatile ("add %1, %0" : "=b" (data) : "c" (port) : "memory");

翻译成汇编指令的结果如下

	movl	8(%ebp), %eax
	movl	%eax, %ecx
#APP
# 4 "test.c" 1
	add %ecx, %ebx
# 0 "" 2
#NO_APP

第二条指令是什么意思呢?

static inline void
insl(uint32_t port, void *addr, int cnt) {
    asm volatile (
        "cld;"
        "repne; insl;"
        : "=D" (addr), "=c" (cnt)
        : "d" (port), "0" (addr), "1" (cnt)
        : "memory", "cc");
}

cld指令是使DF=0, 即si,di寄存器自动增加

SI(Source Index):源变址寄存器可用来存放相对于DS段之源变址指针;
DI(Destination Index):目的变址寄存器,可用来存放相对于 ES 段之目的变址指针。

关于repe指令的相关介绍,
rep指令的目的是重复其上面的指令.ECX的值是重复的次数.repe和repne,前者是repeat equal,意思是相等的时候重复,后者是repeat not equal,不等的时候重复;每循环一次cx自动减一。

insl指令的意思

insl 从 DX 指定的 I/O 端口将双字输入 ES:(E)DI 指定的内存位置

这个函数编译成汇编长什么样子

080483ed <insl>:

static void
insl(int port, void *addr, int cnt) {
 80483ed:	55                   	push   %ebp
 80483ee:	89 e5                	mov    %esp,%ebp
 80483f0:	57                   	push   %edi
 80483f1:	53                   	push   %ebx
    asm volatile (
 80483f2:	8b 55 08             	mov    0x8(%ebp),%edx
 80483f5:	8b 4d 0c             	mov    0xc(%ebp),%ecx
 80483f8:	8b 45 10             	mov    0x10(%ebp),%eax
 80483fb:	89 cb                	mov    %ecx,%ebx
 80483fd:	89 df                	mov    %ebx,%edi
 80483ff:	89 c1                	mov    %eax,%ecx
 8048401:	fc                   	cld    
 8048402:	f2 6d                	repnz insl (%dx),%es:(%edi)
 8048404:	89 c8                	mov    %ecx,%eax
 8048406:	89 fb                	mov    %edi,%ebx
 8048408:	89 5d 0c             	mov    %ebx,0xc(%ebp)
 804840b:	89 45 10             	mov    %eax,0x10(%ebp)
        "cld;"
        "repne; insl;"
        : "=D" (addr), "=c" (cnt)
        : "d" (port), "0" (addr), "1" (cnt)
        : "memory", "cc");
}
 804840e:	5b                   	pop    %ebx
 804840f:	5f                   	pop    %edi
 8048410:	5d                   	pop    %ebp
 8048411:	c3                   	ret    

这个函数与上面唯一不同的地方就在于输入部分多了几个匹配限制字符, 0,1我们就称之为匹配限制符

"0" (addr), "1" (cnt)

而且你发现addr和cnt既出现在输出部分,又出现在输入部分,这是为什么?
我们下面举一个例子进行讲解

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

“r”将输入变量放入通用寄存器,也就是 eax , ebx, ecx,edx, esi, edi 中的一个

他所对应的汇编代码如下

	movl	$0, result
	movl	$1, input
	movl	input, %eax
#APP
# 7 "test.c" 1
	addl %eax,%eax
# 0 "" 2
#NO_APP
	movl	%eax, result
	popl	%ebp

上面这条汇编指令执行的结果是什么,你会发现结果是2,这显然不对啊,结果应该是1啊。出现这样的结果是为什么呢?

因为对于输出部分的变量,对于gcc来讲是输出操作数,所以只会给他分配一个通用寄存器,而不会将原先result的值给取出来做计算,gcc认为输出操作数的原来的值没有用,所以编译上优化掉了。

那这样的话我改成下面这样呢?

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

查看一下他的汇编

	movl	$0, result
	movl	$1, input
	movl	result, %eax
#APP
# 7 "test.c" 1
	addl input, %eax
# 0 "" 2
#NO_APP
	movl	%eax, result

这个看似正常,但其实存在问题,如果result分配的寄存器不是同一个就会出现问题,

movl _result,%edx-----输入部分分配的edx
#APP
addl _input,%eax-------结果还是错误的
#NO_APP
movl %eax,%edx

其实感觉改成这样应该也可以,这个地方只是猜测,没有经过验证。

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

汇编指令如下

	movl	$0, result
	movl	$1, input
	movl	result, %eax
	movl	%eax, %ebx
#APP
# 7 "test.c" 1
	addl input, %ebx
# 0 "" 2
#NO_APP

所以为了解决这个问题
gcc引入了一个匹配限制符,即告诉gcc,这个变量需要一直使用同一个寄存器,写在输入部分是为了要他以前的值。0,1是为了说明和占位符数值相同的变量使用同一个寄存器,因为他们是同一个变量
所以这里的意思输入部分的addr与输出部分的addr是同一个变量,所以他们应该使用同一个寄存器。cnt变量也是同理。

        : "=D" (addr), "=c" (cnt)
        : "d" (port), "0" (addr), "1" (cnt)

所以上面那条嵌入汇编的意思,大概就能清楚了

port变量和edx绑定
cnt变量和ecx绑定
addr 变量和 edi绑定,每次递增完,地址加4

即把端口号为port 传送到 addr,读取cnt个字节

可能讲解的不是很好,但是结合上面我使用的例子,和查看结果的命令,配合下面的文章,我觉得这篇文章写得很好,以上也只是进行了一些概括和提炼。

AT&T汇编语言与GCC内嵌汇编简介.pdf

发布了17 篇原创文章 · 获赞 3 · 访问量 3551

猜你喜欢

转载自blog.csdn.net/sgy1993/article/details/89225075
今日推荐