C内联汇编
之前对C的内联汇编了解较少,最近做毕设的时候想搞个rpc协议,想着可以动态注册C函数以及动态调用C函数,其主要的难点在于动态你调用注册的函数时如何处理参数的问题,顺便把内联汇编学习下。
基于gcc的官方文档Extended Asm (Using the GNU Compiler Collection (GCC))分析, 使用的是x64的 AT&T 汇编格式
基本格式
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
: InputOperands
: Clobbers
: GotoLabels)
asm-qualifiers
这是一个可选项,有两个选择
- volatile 不优化你写的汇编代码, 不指定asm-qualifiers则默认是volatile
- inline 编译器可以优化你写的代码
AssemblerTemplate
汇编语句模板,直接写汇编,叫模板代表里面的部分关键值可以被替换。
$值 将是即时值
%开头 代表被替换
%%寄存器 代表一个寄存器
OutputOperands
单个输出基本格式
[ [asmSymbolicName] ] constraint (cvariablename)
- asmSymbolicName 用于给这个输出变量一个别名,在汇编模板里使用**%[别名]引用,正常是使用%序列号**引用
- constraint 用于指定对这个变量操作的约束的字符串,在输出列表里,这个必须是以**’=’或者’+’**开头,=代表覆盖,+代表读写
- cvariablename 用于指定C语言中对应的变量
constraint常见约束,所有约束见Constraints (Using the GNU Compiler Collection (GCC))
约束符号 | 作用 |
---|---|
r | 操作数将被放到通用寄存器中,先把操作数放到寄存器中再进行操作 |
m | 替换后是操作数在内存中的地址 |
o | 同m,但要求内存地址范围在同一段内 |
V | 同m,但要求内存地址范围不在同一段内 |
多个输出以 ‘,‘分开
InputOperands
单个输入基本格式
[ [asmSymbolicName] ] constraint (cvariablename)
asmSymbolicName 用于给这个输出变量一个别名,在汇编模板里使用**%[别名]引用,正常是使用%序列号**引用
constraint 用于指定对这个变量操作的约束的字符串,在输入列表里,这个必须不以**’=’或者’+’**开头,约束同输出的constraint表
cvariablename 用于指定C语言中对应的变量
多个输出以 ‘,‘分开
输入列表里的任何变量在执行内联汇编语句前后值会保持不变,即使在汇编代码里修改了也一样。
Clobbers
告诉编译器哪些寄存器在你写的汇编代码里被修改了
GotoLabels
用于goto label语句,可以在这里指定要汇编语句模板中的一个地址,label应该使用%l+序号使用
几个例子
使用符号名(寄存器)
#include<stdio.h>
int main(int argc, char const *argv[])
{
int v = 0,new_value = 1;
fprintf(stdout,"before asm: v=%d\n",v);
asm (
"mov %[a],%[v];" //a、v被分别替换
:[v] "=g"(v)//放到寄存器里
:[a] "g"(new_value)//放到寄存器里
:
);
fprintf(stdout,"after asm: v=%d\n",v);
return 0;
}
/*
before asm: v=0
after asm: v=1
*/
使用符号名(内存)
#include<stdio.h>
int main(int argc, char const *argv[])
{
int v = 0,new_value = 1;
fprintf(stdout,"before asm: v=%d\n",v);
asm (
"movl $2, %[v];" //即时值2, AT&T通过movx(x={b,w,l})来给内存赋值
:[v] "=m"(v) //之际放内存地址
:
:
);
fprintf(stdout,"after asm: v=%d\n",v);
return 0;
}
/*
before asm: v=0
after asm: v=2
*/
使用asm goto
#include<stdio.h>
int main(int argc, char const *argv[])
{
asm goto (
"jmp %l0" //用%l+序列号(是小写的L)
:
:
:
:error //指定label
);
fprintf(stdout,"normal\n");
return 0;
error:
fprintf(stdout,"error label\n");
return 1;
}
/*
error label
*/
动态调用函数
目前只是一个demo,毕设里的想法是建一个hashtable,把注册的调用名map到函数的地址,并且在注册时可以指定参数,这样可以很方便地实现rpc协议
x64的函数调用abi
RDFM
文档链接
Passing Once arguments are classified, the registers get assigned (in left-to-right order) for passing as follows:
- If the class is MEMORY, pass the argument on the stack.
- If the class is INTEGER or POINTER, the next available register of the sequence %rdi, %rsi, %rdx, %rcx, %r8 and %r9 is used.
- If the class is SSE, the next available vector register is used, the registers are taken in the order from %xmm0 to %xmm7
…
如果是INTEGER或者POINTER,且参数数量<=6,则可以把参数依次直接放到rdi、rsi、rdx、rcx、r8、r9。
上代码
按照ABI把参数放入对应的寄存器并call函数的地址即可
#include<stdio.h>
#define MAX_ARGC 6
typedef struct{
int len;
void* args[MAX_ARGC];
} rpc_args;
int register_function(const char *fun_name,void* fun);
int call_function(const char *fun_name,rpc_args *args);
void* fun_ptr;
void hello(int a,int b,int c){
fprintf(stdout,"hello a=%d,b=%d,c=%d\n",a,b,c);
}
int main(int argc,const char *argv[]){
register_function("hello",hello);
call_function("hello",NULL);
}
int register_function(const char *fun_name,void* fun){
fun_ptr = fun;
}
int call_function(const char *fun_name,rpc_args *args){
int a = 2,b=1,c=2;
// rdi, rsi, rdx, rcx, r8, r9
asm volatile(
"mov %0,%%rdi;"//arg0
"mov %1,%%rsi;"//arg1
"mov %2,%%rdx;"//arg2
"call *%3;"//call
:
:"g"(a),"g"(b),"g"(c),"m"(fun_ptr)
:"%rdi","%rsi","%rdx"
);
}
/*
hello a=2,b=1,c=2
*/