C与汇编混合编程

c文件与汇编文件

section .data
str: db "asm_print say hello world!", 0xa, 0 ;0xa为换行符,0为字符串结束标志
str_len equ $-str

section .text
extern c_print
global _start     ;将符号导出为全局属性
_start:
    ;调用c代码中函数 c_print
    push str      ;传参
    call c_print  ;调用c函数
    add esp, 4    ;回收栈空间

    ;退出程序
    mov eax, 1
    mov ebx, 0
    int 0x80

global asm_print   ;相当于 `asm_print(str, size)`
asm_print:
    push ebp
    mov ebp, esp
    mov eax, 4     ;write 系统调用号
    mov ebx, 1     ;文件描述符
    mov ecx, [ebp+8]   ;第1个参数
    mov edx, [ebp+12]  ;第2个参数
    int 0x80       ;发起中断
    pop ebp        ;恢复ebp
    ret

 

extern void asm_print(char*, int);

void c_print(char* str) {
  int len = 0;
  while (str[len++])
    ;
  asm_print(str, len);
}

程序入口在汇编代码的 _start 函数

 

内联汇编

分为基础内联汇编和拓展内联汇编。使用AT&T语法。

问题:

  1. 汇编代码与c代码就寄存器等资源的使用冲突如何解决?
  2. 汇编代码如何使用c代码中定义的变量?

解决方法:

  • 使用拓展内联汇编。提供一个模板,让编译器处理。

推荐使用纯汇编文件再链接

AT&T 风格汇编

指令名后加上了操作数大小后缀,1字节b、2字节w、4字节l

内存寻址差异:

Intel 语法,立即数就是普通数字,内存地址使用中括号包含

AT&T语法,立即数为$符加数字,内存地址就是普通数字

内存寻址格式:

segreg{段基址}:base_address+offset_address+index*size

Intel:segreg:base_address(offset_address, index, size)

AT&T:segreg:[base+index*size+offset]各种寻址方式都可以使用这种格式的某部分进行表达

基本内联汇编

格式:asm [volatile] ("assembly code")

asm 与 volatile 都是gcc的关键字,使用 -O 参数可以指定优化级别,使用volatile关键字保证编译器不会修改代码。

汇编代码必须位于圆括号中,且要用双引号括起来。对于 assembly code

  1. 必须用小括号包裹,无论一条还是多条指令
  2. 一堆小括号不能跨行,如果跨行需要用 '\' 转义
  3. 指令之间用 '\n' '\r\n' 进行分割

即使指令分布在多个双引号中,gcc也会将其合并,所以除了最后一条指令必须有分隔符。

int main(int argc, char const *argv[])
{
  asm("mov $9, %rax \n" "push %rax \n" "pop %rax");
  return 0;
}
char* str = "hello,world\n";
int count = 0;

void main() {
  asm(
      // "pusha \n"
      "mov $4, %rax \n"
      "mov $1, %rbx \n"
      "mov str, %rcx \n"  // 书上的例子,但编译时提示无法使用 str 变量。 ???
      "mov $12, %rdx \n"
      "int $0x80 \n"
      "mov %rax, count \n"
      // "popa"
  );
}

扩展内联汇编(32位)

asm [volatile] ("asembly code" : output : input : clobber/modify)

括号内分为4部分,assembly code、output、input、clobber/modify。其中任何一个部分都可以省略。省略一个部分时保留:占位,如果省略的是后面一个或多个连续的部分,分隔符也不用保留。

  1. assembly code:汇编指令,和基本内联汇编一样
    其中内联汇编用于帮c代码完成某些功能,C代码要为其提供参数和存放输出结果的空间。
  2. output:用于指定汇编代码的数据如何输出给C代码使用。汇编结束运行后,想将结果存储到C变量中,就使用此输出位置。
    格式为:”操作数修饰符约束名称“(C变量名) 其中引号和圆括号不能少
    操作数修饰符通常为“=”。多个操作数间用逗号“,”分隔
  3. input:用于指定C中数据如何输入给汇编。
    格式为:"[操作数修饰符] 约束名"(C 变量名)
    其中引号和圆括号不能少,操作修饰符为可选项。多个操作数间用逗号','分隔。
  4. clobber/modigy:汇编代码执行后会破坏一些内存或寄存器资源,通过这项通知编译器,可能造成寄存器或内存数据破坏,这样gcc就知道哪些寄存器或内存需要提前保护起来。

assembly code 中引用所有操作数是经过gcc转换过的复本,“原件”在c变量中。其中单个 % 用于表示占位符。

上述“要求”又叫“约束”,其将C代码中操作数映射为汇编中使用的操作数,实际就是描述C中操作数如何变汇编操作数。这些约束作用域是input和output部分。

约束分4大类:

  1. 寄存器约束
  2. 内存约束
  3. 立即数约束
  4. 通用约束

1. 寄存器约束

a:寄存器 eax/ax/al
b: ebx/bx/bl
c: ecx/cx/cl
d: edx/dx/dl

D: edi/di
S: esi/si
q: eax/ebx/ecx/edx
r: eax/ebx/edx/edx/esi/edi
g: 可以存放到任意地点(寄存器或内存)

A: 把eax和edx组合成64位整数
f: 表示浮点寄存器
t: 第1个浮点寄存器
u: 第2个浮点寄存器

#include <stdio.h>

int main() {
  int in_a = 1, in_b = 2, out_sum = 0;
  asm("add %%ebx, %%eax" : "=a"(out_sum) : "a"(in_a), "b"(in_b));
  printf("sum is %d\n", out_sum);

  return 0;
}
/*
其中寄存器前缀是两个%
对于输入变量,用约束名a为变量in_a指定了eax,为in_b指定了ebx
对于输出,使用约束名指定了eax,修饰符‘+’表示只写
*/

 

2. 内存约束

要求gcc将变量地址作为内联汇编的操作数,不用寄存器中转。

m: 表示操作数可用于任意一种内存形式
o: 操作数为内存变量,但访问它是通过偏移量的形式访问,即包含 offset_address 的格式

#include <stdio.h>

int main() {
  int in_a = 1, in_b = 2;
  printf("in_b is %d\n", in_b);
  asm("mov %b0, %1" ::"a"(in_a), "m"(in_b));
  printf("in_b now is %d\n", in_b);

  return 0;
}
/*
作用是用in_a的值替换in_b
把 in_a 施加寄存器约束a,告诉gcc把变量 in_a 放到寄存器 eax 中,对 in_b 施加内存约束 m,告诉gcc把 in_b 的指针作为内联代码的操作数
“%1”为序号占位符,这里代表 in_b 的指针
“%b0”使用数据的低8位
*/
/*
0000000000001139 <main>:
    1139:       55                      push   %rbp
    113a:       48 89 e5                mov    %rsp,%rbp
    113d:       48 83 ec 10             sub    $0x10,%rsp
    1141:       c7 45 fc 01 00 00 00    movl   $0x1,-0x4(%rbp)         # 变量 in_a
    1148:       c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%rbp)         # 变量 in_b
    114f:       8b 45 f8                mov    -0x8(%rbp),%eax
    1152:       89 c6                   mov    %eax,%esi
    1154:       48 8d 05 a9 0e 00 00    lea    0xea9(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    115b:       48 89 c7                mov    %rax,%rdi
    115e:       b8 00 00 00 00          mov    $0x0,%eax
    1163:       e8 c8 fe ff ff          call   1030 <printf@plt>
    1168:       8b 45 fc                mov    -0x4(%rbp),%eax
    116b:       88 45 f8                mov    %al,-0x8(%rbp)          # 内联汇编,将al写入in_b
    116e:       8b 45 f8                mov    -0x8(%rbp),%eax
    1171:       89 c6                   mov    %eax,%esi
    1173:       48 8d 05 96 0e 00 00    lea    0xe96(%rip),%rax        # 2010 <_IO_stdin_used+0x10>
    117a:       48 89 c7                mov    %rax,%rdi
    117d:       b8 00 00 00 00          mov    $0x0,%eax
    1182:       e8 a9 fe ff ff          call   1030 <printf@plt>
    1187:       b8 00 00 00 00          mov    $0x0,%eax
    118c:       c9                      leave
    118d:       c3                      ret
*/

 

3. 立即数约束

i: 整数
F: 浮点数
I: 0 ~ 31
J: 0 ~ 63
N: 0 ~ 255
O: 0 ~ 32
X: 任何类型立即数

 

4. 通用约束

0 ~ 9:用在input部分,表示可与output和input中第n个操作数用相同的寄存器或内存。

 

有时对寄存器没有要求,用哪个都可以。使用 r 作为约束,此时不知道gcc为操作数分配了哪个寄存器,或内存约束的操作数没有名称可用。
为了解决对操作数的引用,扩展内联汇编提供了占位符,用于代表约束指定的操作数(寄存器、内存、立即数),更多的是在内联汇编中使用占位符来引用操作数。分序号占位符名称占位符两种。占位符使用 % 作前缀,寄存器使用 %% 作前缀。

1. 序号占位符

对output和input中操作数按照从左到右的次序从0开始编号,格式为 %0 ~ %9。注意,指代的是汇编中的操作数,而不是圆括号中的C变量。

例如:asm("add %2, %1":"=a"(out_sum): "a"(in_a), "b"(in_b));
"=a"(out_sum)   序号为 0,%0对应 eax
"a"(in_a),            序号为 1,%1对应 eax
"b"(in_b)         序号为 2,%2对应 ebx

在%和序号之间可以使用b指定使用0~7位例如 al,用 h 指定 8 ~ 15 例如 ah。

#include <stdio.h>

int main() {
  int in_a = 0x12345678, in_b = 0;

  asm("mov %1, %0" : "=m"(in_b) : "a"(in_a));
  printf("double word in_b is 0x%x\n", in_b);
  in_b = 0;

  asm("movw %w1, %w0" : "=m"(in_b) : "a"(in_a));
  printf("word in_b is 0x%x\n", in_b);
  in_b = 0;

  asm("movb %b1, %b0" : "=m"(in_b) : "a"(in_a));
  printf("low byte in_b is 0x%x\n", in_b);
  in_b = 0;

  asm("movb %h1, %0" : "=m"(in_b) : "a"(in_a));
  printf("high byte in_b is 0x%x\n", in_b);

  return 0;
}
/*
$ gcc  tmp.c && ./a.out
double word in_b is 0x12345678
word in_b is 0x5678
low byte in_b is 0x78
high byte in_b is 0x56
*/

 

2. 名称占位符

在output和input中使用 [名称]"约束名"(C 变量) 的方式为操作数显示起名,在汇编代码中使用 %[名称] 的形式使用。

#include <stdio.h>

int main() {
  int in_a = 18, in_b = 3, out = 0;

  asm("divb %[divisor] \n movb %%al, %[result]"
      : [result] "=m"(out)
      : "a"(in_a), [divisor] "m"(in_b));
  printf("result is %d\n", out);
  return 0;
}
// result is 6

 

无论使用哪种占位符,它都是指代C变量经过约束后、由gcc分配的对应于汇编代码中的操作符,与C变量本身无关。

约束中还有以下几种操作数类型修饰符,用来修饰所约束的操作数:内存、寄存器,分别在 output 和 input 中有以下几种。

output中有以下3种:

  1. =  表示操作数只写
  2. +  操作数可读写,告诉gcc其被先读入再被写入
  3. &  表示操作数要独占所分配的寄存器,只供 output 使用,任何 input 所分配的寄存器不能与此相同。当有多个修饰符时,&与约束名紧挨着。

input种

  1. %  该操作数可与下一个操作数互换

 

clobber/modify 部分

用于通知gcc修改了哪些寄存器或内存。在output和input种约束指定的寄存器gcc必然知道,所以需要在 clobber/modify 中通知的寄存器和内存不在 output 和 input 种出现过。

例如: asm("mov %%eax, %0 \n movl %%eax, %%ebx":"=m"(ret_value)::"bx"); 一个寄存器可以使用 al / ax / eax / qax 。

如果修改了内存且没有直接在汇编语句中出现,要在 clobber/modigy 中 “memory”声明。另一个原因是清除寄存器缓存:内存中的变量在寄存器中有缓冲,可以在定义变量时使用 volatile 关键字解决此问题;在扩展内联汇编中使用“memory”。

posted @ 2022-10-05 20:03  某某人8265  阅读(156)  评论(0编辑  收藏  举报