0%

操作系统:gcc扩展内联汇编基础

gcc

gcc就不细说了

1
$ gcc -Wall hello.c -o hello

选项 -Wall 开启编译器几乎所有常用的警告。

编译器有很多其他的警告选项,但 -Wall 是最常用的。默认情况下GCC ghg产生任何警告信息。

当编写 C 或 C++ 程序时编译器警告非常有助于检测程序存在的问题。

AT&T汇编基本语法

文档里的这个比较写得很好了,注意的点在于AT&T与汇编的赋值语句顺序是反的,这个得相当注意

1
2
3
4
5
6
7
8
9
10
11
12
13
* 寄存器命名原则
AT&T: %eax Intel: eax
* 源/目的操作数顺序
AT&T: movl %eax, %ebx Intel: mov ebx, eax
* 常数/立即数的格式 
AT&T: movl $_value, %ebx Intel: mov eax, _value
把value的地址放入eax寄存器
AT&T: movl $0xd00d, %ebx Intel: mov ebx, 0xd00d
* 操作数长度标识
AT&T: movw %ax, %bx Intel: mov bx, ax
* 寻址方式
AT&T: immed32(basepointer, indexpointer, indexscale)
Intel: [basepointer + indexpointer × indexscale + imm32)

对于AT&T的寻址操作我表示有点迷,不过照着文档写应该是没有问题的了

1
2
3
4
5
6
7
8
9
10
11
* 直接寻址 
AT&T: foo Intel: [foo]
boo是一个全局变量。注意加上$是表示地址引用,不加是表示值引用。对于局部变量,可以通过堆栈指针引用。

* 寄存器间接寻址
AT&T: (%eax) Intel: [eax]

* 变址寻址
AT&T: _variable(%eax) Intel: [eax + _variable]
AT&T: _array( ,%eax, 4) Intel: [eax × 4 + _array]
AT&T: _array(%ebx, %eax,8) Intel: [ebx + eax × 8 + _array]

gcc基本内联汇编

什么是内联汇编(Inline assembly)?

  1、这是GCC对C语言的扩张,就是在C代码里面去写汇编代码

  2、可以直接在C的语句中插入汇编指令

有何用处?

  1、C语言不足以完成所有CPU的指令, 特别是有一些特权指令,比如加载gdt表(Global Descriptor Table 全局描述符表),从而使用汇编代码来完成

  2、用汇编在C语言中手动优化,特别是在操作系统当中,使用汇编对操作系统的掌控更为精准,更加准确。

如何工作?

  1、用给定的模板和约束来生成汇编指令

  2、才C函数内形成汇编代码

1
2
3
4
asm( "pushl %eax\n\t"
"movl $0,%eax\n\t"
"popl %eax"
);

由于gcc会把汇编的代码统一交给GAS汇编,但是如果在这个汇编的代码的上下文中有用到被汇编代码动过的寄存器,会出现问题。

简单来讲,就是gcc把汇编交给GAS后往下执行的时候并不知道这些寄存器是被改变过的,所以我们需要扩展内联汇编

扩展内联汇编

在基本内联汇编中,我们只有指令。然而在扩展汇编中,我们可以同时指定操作数。它允许我们指定输入寄存器、输出寄存器以及修饰寄存器列表。GCC 不强制用户必须指定使用的寄存器。我们可以把头疼的事留给 GCC ,这可能可以更好地适应 GCC 的优化。不管怎么说,基本格式为:

1
2
3
4
5
asm ( 汇编程序模板
: 输出操作数 /* 可选的 */
: 输入操作数 /* 可选的 */
: 修饰寄存器列表 /* 可选的 */
);

汇编程序模板由汇编指令组成。每一个操作数由一个操作数约束字符串所描述,其后紧接一个括弧括起的 C 表达式。冒号用于将汇编程序模板和第一个输出操作数分开,另一个(冒号)用于将最后一个输出操作数和第一个输入操作数分开(如果存在的话)。逗号用于分离每一个组内的操作数。总操作数的数目限制在 10 个,或者机器描述中的任何指令格式中的最大操作数数目,以较大者为准。

它代表什么含义呢?这需要从其基本格式讲起。GCC扩展内联汇编的基本格式是:

1
2
3
4
asm [volatile] ( Assembler Template
: Output Operands
[ : Input Operands
[ : Clobbers ] ])

其中,asm 表示汇编代码的开始,其后可以跟 volatile(这是可选项),其含义是避免 “asm” 指令被删除、移动或组合,在执行代码时,如果不希望汇编语句被 gcc 优化而改变位置,就需要在 asm 符号后添加 volatile 关键词:asm volatile(…);或者更详细地说明为:asm volatile(…);然后就是小括弧,括弧中的内容是具体的内联汇编指令代码。 “” 为汇编指令部分,例如,”movl %%cr0,%0\n\t”。数字前加前缀 “%“,如%1,%2等表示使用寄存器的样板操作数。可以使用的操作数总数取决于具体CPU中通用寄存器的数 量,如Intel可以有8个。指令中有几个操作数,就说明有几个变量需要与寄存器结合,由gcc在编译时根据后面输出部分和输入部分的约束条件进行相应的处理。由于这些样板操作数的前缀使用了”%“,因此,在用到具体的寄存器时就在前面加两个“%”,如%%cr0。输出部分(output operand list),用以规定对输出变量(目标操作数)如何与寄存器结合的约束(constraint),输出部分可以有多个约束,互相以逗号分开。每个约束以“=”开头,接着用一个字母来表示操作数的类型,然后是关于变量结合的约束。例如,上例中:

1
:"=r" (__dummy)

“=r”表示相应的目标操作数(指令部分的%0)可以使用任何一个通用寄存器,并且变量__dummy 存放在这个寄存器中,但如果是:

:“=m”(__dummy)

“=m”就表示相应的目标操作数是存放在内存单元__dummy中。表示约束条件的字母很多,下表给出几个主要的约束字母及其含义:

1
2
3
4
5
6
7
8
9
10
字母	含义
m, v, o 内存单元
R 任何通用寄存器
Q 寄存器eax, ebx, ecx,edx之一
I, h 直接操作数
E, F 浮点数
G 任意
a, b, c, d 寄存器eax/ax/al, ebx/bx/bl, ecx/cx/cledx/dx/dl
S, D 寄存器esiedi
I 常数(031

概括一下,扩展的意思就是在原本的汇编代码后面要加上被这段汇编代码用到的寄存器和变量,便于GCC优化
下面是最通俗易懂的例子

1
2
3
4
5
6
7
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);

这里我们使用汇编指令让“b”的值等于“a”的值。下面是
一些要点:
“b”是一个输出操作数,通过%0联系起来;而“a”是一个输入操作数,通过%1联系起来。“r”是对操作数的一个约束。“r”告诉GCC使用任何一个寄存器来存储操作数的值。输出操作数的约束必须包含一个约束修饰符“=”。这个修饰符说明输出操作数是只写的。

这里有两个“%”在寄存器之前。这样可以帮助GCC辨别操作数和寄存器,操作数只有一个“%”作为前缀。第三个冒号后的clobbered register %eax告诉GCC%eax的值将会在“asm”里被修改,所以GCC不会使用这个寄存器去存储其它的数值

当“asm”程序执行完后,“b”会映射出更新后的值,因为它被指定为一个输出操作数。换句话说,在“asm”里对“b”的改变将会影响到“asm”的外面的程序。

make和Makefile

GNU make(简称make)是一种代码维护工具,在大中型项目中,它将根据程序各个模块的更新情况,自动的维护和生成目标代码。

make命令执行时,需要一个 makefile (或Makefile)文件,以告诉make命令需要怎么样的去编译和链接程序。首先,我们用一个示例来说明makefile的书写规则。以便给大家一个感兴认识。这个示例来源于gnu的make使用手册,在这个示例中,我们的工程有8个c文件,和3个头文件,我们要写一个makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:

  • 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。
  • 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。
  • 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序。

只要我们的makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。