C和汇编
许多程序员更习惯用C语言编写,并且有充分的理由:C是一种中级语言(与Assembly相比,它是一种低级语言),并且使程序员省去了实际实现的一些细节。
但是,有一些低级任务要么可以在汇编中更好地实现,要么只能用汇编语言实现。此外,程序员通常可以查看C编译器的汇编输出,并手动编辑或手动编译汇编代码,而编译器则无法使用。汇编对于时间关键或实时流程也很有用,因为与高级语言不同,对于如何编译代码没有任何歧义。可以严格控制时序,这对于编写简单的设备驱动程序很有用。本节将介绍混合C和汇编程序开发的多种技术。
内联汇编
在C编程项目中使用汇编代码片段的最常用方法之一是使用称为内联汇编的技术。内联汇编以不同的方式在不同的编译器中调用。此外,内联汇编中使用的汇编语言语法完全取决于C编译器使用的汇编引擎。例如,Microsoft C ++仅接受MASM语法中的内联汇编命令,而GNU GCC仅接受GAS语法中的内联汇编(也称为AT&T语法)。本页将讨论一些常见编译器中混合语言编程的一些基础知识。
Microsoft C编译器
#include <stdio.h> void main () { int a = 3 , b = 3 , c ; asm { mov ax ,a mov bx ,b add ax ,bx mov c ,ax } printf (“%d” , c ); }
GNU GCC编译器
#include <stdio.h> #include <stdint.h> int main (int argc , char ** argv ) { int32_t var1 = 10 , var2 = 20 , sum = 0 ; asm volatile (“addl %% ebx,%% eax;” : “= a” (sum ) / * output:sum = EAX * / : “a” (var1 ), “b” (var2 ) / *输入:EAX = var1,EBX = var2 * / ); printf (“sum =%d \ n ”, 总和); 返回 0 ; }
Borland C编译器
链接大会
当汇编程序汇编汇编源文件,C编译器编译C源文件时,这两个目标文件可以通过链接器链接在一起形成最终的可执行文件。这种方法的优点是可以使用程序员熟悉的任何语法和汇编程序编写汇编文件。此外,如果需要在汇编代码中进行更改,则所有代码都存在于单独的文件中,程序员可以轻松访问。以这种方式混合汇编和C的唯一缺点是:a)汇编器和编译器都需要运行,b)这些文件需要由程序员手动链接在一起。这些额外的步骤相对容易,但它确实意味着程序员需要学习编译器,汇编器和链接器的命令行语法。
内联汇编与链接汇编
内联装配的优点:
简短的汇编程序可以直接嵌入C函数的C代码文件中。然后可以使用单个命令将混合语言文件完全编译到C编译器(而不是使用汇编程序编译汇编代码,使用C编译器编译C代码,然后将它们链接在一起)。这种方法快速简便。如果内联汇编嵌入在函数中,那么即使将编译器切换到不同的调用约定,程序员也不必担心#Calling_Conventions。
链接装配的优点:
如果选择了新的微处理器,则所有汇编命令都将在“.asm”文件中隔离。程序员可以只更新那个文件 – 不需要更改任何“.c”文件(如果它们是可移植的)。
呼叫约定
在编写单独的C和汇编模块并将它们与链接器链接时,重要的是要记住许多高级C构造都是非常精确定义的,并且需要由程序的汇编部分正确处理。也许混合语言编程的最大障碍是函数调用约定的问题。C函数都是根据程序员选择的特定约定实现的(如果您从未“选择”特定的调用约定,那是因为您的编译器具有默认设置)。该页面将介绍程序员可能遇到的一些常见调用约定,并将描述如何使用汇编语言实现这些约定。
使用一个编译器编译的代码在链接到使用不同调用约定编译的代码时将无法正常工作。如果代码是用C或其他高级语言(或嵌入到C函数中的汇编语言),这是一个小麻烦 – 程序员需要选择她今天想要使用的编译器/优化开关,并重新编译每个程序的一部分。将汇编语言代码转换为使用不同的调用约定需要更多的手动操作,并且更容易出错。
不幸的是,调用约定通常不同于一个编译器,即使在同一个CPU上也是如此。有时,调用约定会从一个版本的编译器更改为下一个版本,或者甚至在给定不同的“优化”开关时从同一编译器更改。
不幸的是,很多时候特定版本的特定编译器使用的调用约定没有充分记录。因此,汇编语言程序员不得不使用逆向工程技术来确定他们需要知道的确切细节,以便调用用C编写的函数,并接受来自用C编写的函数的调用。
典型的过程是:
- 写一个带有存根的“.c”文件…详情??? …… …您希望汇编语言函数具有的输入和输出的数量和类型完全相同。
- 使用适当的开关编译该文件,以提供混合的assembly-language-with-c-in-comments文件(通常为“.cod”文件)。(如果您的编译器无法生成汇编语言文件,则需要重新组装二进制“.obj”机器代码文件)。
- 将“.cod”文件复制到“.asm”文件。(有时你需要删除已编译的十六进制数字并注释掉其他行以将其转换为汇编程序可以处理的内容)。
- 测试调用约定 – 将“.asm”文件编译为“.obj”文件,并将其(而不是存根“.c”文件)链接到程序的其余部分。测试看“呼叫”是否正常工作。
- 填写“.asm”文件 – “。asm”文件现在应该在每个函数上包含适当的页眉和页脚,以正确实现调用约定。在函数中间注释掉存根代码,并使用汇编语言实现填写函数。
- 测试。通常,程序员单步执行新代码中的每条指令,确保它执行他们想要的操作。
参数传递
通常,参数通过堆栈在函数之间传递(在C中或在汇编中写入)。例如,如果函数foo1()使用2个参数(比如字符x和y)调用函数foo2(),那么在控件跳转到foo2()的开头之前,两个字节(大多数字符的正常大小)系统)充满了需要传递的值。一旦控制跳转到新函数foo2(),并使用函数中的值(作为参数传递),就会从堆栈中检索并使用它们。
有两种参数传递技术在使用,1.通过价值2.通过参考
参数传递技术也可以使用从右到左(C风格)从左到右(帕斯卡风格)
在具有大量寄存器的处理器(例如ARM和Sparc)上,标准调用约定将* all *参数(甚至返回地址)放入寄存器中。
在寄存器数量不足的处理器(例如80×86和M8C)上,所有调用约定都被强制在堆栈或RAM中的其他位置放置至少一些参数。
一些调用约定允许“重入代码”。
通过价值
使用pass-by-value,传递实际值(文字内容)的副本。例如,如果你有一个接受两个字符的函数,比如
void foo(char x, char y){
x = x + 1;
y = y + 2;
putchar(x);
putchar(y);
}
并按如下方式调用此函数
char a,b;
a='A';
b='B';
foo(a,b);
然后程序在调用函数foo之前将’A’和’B’的ASCII值(分别为65和66)复制到堆栈上。你可以看到函数foo()中没有提到变量’a’或’b’。因此,您对foo中这两个值所做的任何更改都不会影响调用函数中a和b的值。
通过参考
想象一下,您必须将大量数据传递给函数并将在该函数中完成的修改应用于原始变量。这种情况的一个示例可能是将具有小写字母的字符串转换为大写字母的函数。将整个字符串(特别是如果它是一个大字符串)传递给函数是一个不明智的决定,并且当转换完成时,将整个结果传递回调用函数。这里我们将变量的地址传递给函数。这有两个好处,一个是,你不必传递大量数据,节省执行时间和两个,你可以立即处理数据,这样在函数结束时,调用函数中的数据已经被修改。但请记住,您对通过引用传递的变量所做的任何更改都将导致原始变量被修改。如果这不是您想要的,那么您必须在调用函数之前手动复制变量。
80×86 /奔腾
在当前基于32位和64位的处理器之前,80×86架构使用了复杂的分段存储器模型(也称为实模式)。在大多数情况下,除非将特别低级别的代码直接与硬件或外围芯片接口,否则不会遇到这种情况。
通常编写现代代码以支持“保护模式”,其中存储空间可以被认为是平坦的。
以下信息与受保护模式下的调用约定有关。
CDECL
在CDECL呼叫惯例中,以下内容成立:
- 参数以从右到左的顺序在堆栈上传递,返回值在eax中传递。
- 在调用函数清理堆栈。这允许CDECL函数具有可变长度的参数列表(也称为可变参数函数)。由于这个原因,编译器不会将参数的数量附加到函数的名称,因此汇编器和链接器无法确定是否使用了不正确数量的参数。
变量函数通常具有特殊的入口代码,由va_start(),va_arg()C伪函数生成。
请考虑以下C指令:
_cdecl int MyFunction1 (int a , int b ) { return a + b ; }
和以下函数调用:
X = MyFunction1 (2 , 3 );
这些将分别产生以下汇编列表:
:_MyFunction1 push ebp mov ebp , esp mov eax , [ ebp + 8 ] mov edx , [ ebp + 12 ] add eax , edx pop ebp ret
和
push 3 push 2 call _MyFunction1 add esp , 8
当转换为汇编代码时,CDECL函数几乎总是以下划线为前缀(这就是为什么之前的所有示例都在汇编代码中使用了“_”)。
STDCALL
STDCALL,也称为“WINAPI”(以及一些其他名称,取决于您阅读它的位置)几乎完全由Microsoft使用,作为Win32 API的标准调用约定。由于STDCALL是由Microsoft严格定义的,因此实现它的所有编译器都以相同的方式执行。
- STDCALL从右向左传递参数,并返回eax中的值。(Microsoft文档错误地声称参数是从左到右传递的,但事实并非如此。)
- 与CDECL不同,被调用函数清理堆栈。这意味着STDCALL不允许可变长度的参数列表。
考虑以下C函数:
_stdcall int MyFunction2 (int a , int b ) { return a + b ; }
和调用指令:
X = MyFunction2 (2 , 3 );
这些将产生以下各自的汇编代码片段:
:_MyFunction @ 8 push ebp mov ebp , esp mov eax , [ ebp + 8 ] mov edx , [ ebp + 12 ] add eax , edx pop ebp ret 8
和
按 3 按 2 调用 _MyFunction @ 8
这里有几点需要注意:
- 在函数体中,ret指令有一个(可选)参数,指示函数返回时从堆栈中弹出多少字节。
- STDCALL函数使用前导下划线进行名称修饰,后跟@,然后是堆栈上传递的参数的数量(以字节为单位)。在32位对齐的机器上,此数字始终是4的倍数。
FASTCALL
FASTCALL调用约定并非在所有编译器中都是完全标准的,因此应谨慎使用。在FASTCALL中,前2或3个32位(或更小)参数在寄存器中传递,最常用的寄存器是edx,eax和ecx。其他参数或大于4字节的参数在堆栈上传递,通常以从右到左的顺序(类似于CDECL)。如果需要,最常用的调用函数负责清理堆栈。
由于含糊不清,建议仅在具有1,2或3个32位参数的情况下使用FASTCALL,其中速度至关重要。
以下C函数:
_fastcall int MyFunction3 (int a , int b ) { return a + b ; }
和以下C函数调用:
X = MyFunction3 (2 , 3 );
将分别为被调用函数和调用函数生成以下汇编代码片段:
:@ MyFunction3 @ 8 push ebp mov ebp , esp ;很多编译器创建一个堆栈帧,即使它没有使用 add eax , edx ; a是在eax中,b是在edx pop中 ebp ret
和
;调用函数 mov eax , 2 mov edx , 3 调用 @ MyFunction3 @ 8
FASTCALL的名称修饰在函数名称前面加上@,并在函数名称后跟@x,其中x是传递给函数的参数的数字(以字节为单位)。
许多编译器仍为FASTCALL函数生成堆栈帧,尤其是在FASTCALL函数本身调用另一个子例程的情况下。但是,如果FASTCALL函数不需要堆栈帧,则优化编译器可以省略它。
ARM
主页:嵌入式系统/ ARM微处理器
几乎所有使用ARM处理器的人都使用标准调用约定。与其他处理器相比,这使得混合C和ARM汇编编程相当容易。Thumb函数最简单的进入和退出序列是:[2]
an_example_subroutine: PUSH { save-registers , lr } ; 单行录入序列 ; ...函数的第一部分... BL thumb_sub ;必须在+/- 4 MB的空间中 ; ...函数的其余部分在这里,也许包括其他函数调用 ; 以某种方式在返回 POP { save-registers , pc } 之前获取a1(r0)中的返回值; 单行返回序列
M8C
AVR
C编译器使用哪些寄存器?
数据类型
char是8位,int是16位,long是32位,long long是64位,float和double是32位(这是唯一支持的浮点格式),指针是16位(函数指针是字地址,到允许使用> 64 KB的闪存ROM寻址ATmega器件上的整个128K程序存储空间。有一个-mint8选项(参见C编译器avr-gcc的选项)来生成int 8位,但avr-libc不支持它并且违反C标准(int必须至少为16位)。它可能会在将来的版本中删除。
呼叫使用寄存器(r18-r27,r30-r31)
可以由GNU GCC分配给本地数据。您可以在汇编程序子程序中自由使用它们。调用C子例程可以破坏它们中的任何一个 – 调用者负责保存和恢复。
呼叫保存寄存器(r2-r17,r28-r29)
可以由GNU GCC分配给本地数据。调用C子例程会使它们保持不变。汇编程序子程序负责保存和恢复这些寄存器(如果已更改)。r29:如果需要,r28(Y指针)用作帧指针(指向堆栈上的本地数据)。要求被调用者保存/保存这些寄存器的内容甚至适用于编译器为参数传递分配它们的情况。
固定寄存器(r0,r1)
GNU GCC从未为本地数据分配,但通常用于固定目的:
r0 – 临时寄存器,可被任何C代码(除了保存它的中断处理程序除外)破坏,可用于在一段汇编代码中记忆一段时间
r1 – 假定在任何C代码中始终为零,可用于在一段汇编代码中记忆一段时间,但必须在使用后清除(clr r1)。这包括使用[f] mul [s [u]]指令,它们将结果返回到r1:r0。中断处理程序在进入时保存并清除r1,并在退出时恢复r1(如果它不为零)。
函数调用约定
参数 – 从左到右分配,r25到r8。所有参数都对齐以在偶数寄存器中开始(奇数大小的参数,包括char,在它们上面有一个空闲寄存器)。这样可以更好地利用增强型核心上的movw指令。
如果太多,那些不适合的那些将被传递到堆栈中。
返回值:r24中的8位(不是r25!),r25中的16位:r24,r22-r25中最多32位,r18-r25中最多64位。调用者将8位返回值零/符号扩展为16位(unsigned char比signed char更有效 – 只是clr r25)。带有可变参数列表(printf等)的函数的参数都在堆栈上传递,char被扩展为int。
警告:在2000-07-01之前没有这样的对齐,包括gcc-2.95.2的旧补丁。检查旧的汇编程序子程序,并相应地进行调整。
Microchip PIC
遗憾的是,在为PIC PIC编写程序时使用了几种不同的(不兼容的)调用约定。
PIC架构的几个“特性”使得大多数子程序调用需要几条指令 – 比许多其他处理器上的单条指令更冗长。
调用约定必须处理:
- “分页”闪存程序存储器架构
- 硬件堆栈的限制(可能通过在软件中模拟堆栈)
- “分页”RAM数据存储器架构
- 确保中断例程调用子程序不会在中断返回主循环后加扰所需的信息。
68HC11
Sparc
Sparc有特殊的硬件支持一个很好的调用约定:
一个“注册窗口”……