GCC 内嵌汇编 (中篇)
时间:2010-12-02 来源:CUKdd
内核代码绝大部分使用 C 语言编写,只有一小部分使用汇编语言编写,例如与特定体系结构相关的代码和对性能影响很大的代码。GCC 提供了内嵌汇编的功能,可以在 C 代码中直接内嵌汇编语言语句,大大方便了程序设计。简单的内嵌汇编很容易理解,例:
__asm__ __volatile__("hlt"); “__asm__”
表示后面的代码为内嵌汇编,“asm”是“__asm__”的别名。 “__volatile__” 表示编译器不要优化代码,后面的指令保留原样,“volatile”是它的别名。括号里面是汇编指令。
接上篇!
3.4 限制字符
3.4.1 限制字符列表
限制字符有很多种,有些是与特定体系结构相关,此处仅列出常用的限定字符和 i386中可能用到的一些常用的限定符。它们的作用是指示编译器如何处理其后的 C 语言变量与指令操作数之间的关系,例如是将变量放在寄存器中还是放在内存中等,下表列出了常用的限定字母。
这里有一个问题:假设 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”操作数是一个合法的内存地址(指针)
& 该输出操作数不能使用过和输入操作数相同的寄存器,后面有详细描述。
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 破坏描述部分
接下篇...
__asm__ __volatile__("hlt"); “__asm__”
表示后面的代码为内嵌汇编,“asm”是“__asm__”的别名。 “__volatile__” 表示编译器不要优化代码,后面的指令保留原样,“volatile”是它的别名。括号里面是汇编指令。
接上篇!
3.4 限制字符
3.4.1 限制字符列表
限制字符有很多种,有些是与特定体系结构相关,此处仅列出常用的限定字符和 i386中可能用到的一些常用的限定符。它们的作用是指示编译器如何处理其后的 C 语言变量与指令操作数之间的关系,例如是将变量放在寄存器中还是放在内存中等,下表列出了常用的限定字母。
- 通用寄存器
这里有一个问题:假设 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”操作数是一个合法的内存地址(指针)
- 寄存器或内存
- 匹配
& 该输出操作数不能使用过和输入操作数相同的寄存器,后面有详细描述。
- 操作数类型
- 浮点数
- 浮点寄存器
- 浮点常数
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 破坏描述部分
接下篇...
相关阅读 更多 +