https://zh.wikipedia.org/wiki/调用约定
在计算机科学中, 调用约定是一种定义子过程从调用处接受参数以及返回结果的方法的约定。不同调用约定的区别在于:
- 参数和返回值放置的位置(在寄存器中;在调用栈中;两者混合)
- 参数传递的顺序(或者单个参数不同部分的顺序)
- 调用前设置和调用后清理的工作,在调用者和被调用者之间如何分配
- 被调用者可以直接使用哪一个寄存器有时也包括在内。(否则的话被当成ABI的细节)
- 哪一个寄存器被当作volatile的或者非volatile的,并且如果是volatile的,不需要被调用者恢复
https://zh.wikipedia.org/wiki/应用二进制接口
在软件开发中,应用程序机器二元码接口(英语:application binary interface,缩写为ABI)是指两程序模块间的接口;通常其中一个程序模块会是库或操作系统所提供的服务,而另一边的模块则是用户所运行的程序。
一个ABI定义了机器代码如何访问数据结构与运算程序,此处所定义的界面相当低级并且相依于硬件。而类似概念的API则在源代码定义这些,则较为高端,并不直接相依于硬件,通常会是人类可阅读的代码。一个ABI常见的样貌即是调用约定:数据怎么成为计算程序的输入或者从中得到输出;x86的调用约定即是一个ABI的例子。
决定要不要采取既定的ABI(不论是否由官方提供),通常由编译器,操作系统或库的开发者来决定;然而,如果撰写一个混和多个编程语言的应用程序,就必须直接处理ABI,采用外部函数调用来达成此目的。
描述
ABI涵盖了各种细节,如:
- 数据类型的大小、布局和对齐;
- 调用约定(控制着函数的参数如何传送以及如何接受返回值),例如,是所有的参数都通过栈传递,还是部分参数通过寄存器传递;哪个寄存器用于哪个函数参数;通过栈传递的第一个函数参数是最先push到栈上还是最后;
- 系统调用的编码和一个应用如何向操作系统进行系统调用;
- 以及在一个完整的操作系统ABI中,目标文件的二进制格式、程序库等等。
一个完整的ABI,像Intel二进制兼容标准(iBCS)[1],允许支持它的操作系统上的程序不经修改在其他支持此ABI的操作系统上运行。
其他的ABI标准化了一些细节,包括C++ 名称修饰[2] ,和同一个平台上的编译器之间的调用约定[3],但是不包括跨平台的兼容性。
ABI不同于应用程序接口(API),API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译,然而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。 在Unix风格的操作系统中,存在很多运行在同一硬件平台上互相相关但是不兼容的操作系统(尤其是Intel 80386兼容系统)。有一些努力尝试标准化ABI,以减少销售商将程序移植到其他系统时所需的工作。然而,直到现在还没有很成功的例子,虽然Linux标准化工作组正在为Linux做这方面的努力。
EABI
嵌入式应用二进制接口指定了文件格式、数据类型、寄存器使用、堆积组织优化和在一个嵌入式软件中的参数的标准约定。开发者使用自己的汇编语言也可以使用EABI作为与兼容的编译器生成的汇编语言的接口。 支持EABI的编译器创建的目标文件可以和使用类似编译器产生的代码兼容,这样允许开发者链接一个由不同编译器产生的库。EABI与关于通用计算机的ABI的主要区别是应用程序代码中允许使用特权指令,不需要动态链接(有时是禁止的),和更紧凑的堆栈帧组织用来节省内存。[4] 广泛使用EABI的有Power PC[5]和ARM.[6][7]
https://zh.wikipedia.org/wiki/调用约定
在计算机科学中, 调用约定是一种定义子过程从调用处接受参数以及返回结果的方法的约定。不同调用约定的区别在于:
- 参数和返回值放置的位置(在寄存器中;在调用栈中;两者混合)
- 参数传递的顺序(或者单个参数不同部分的顺序)
- 调用前设置和调用后清理的工作,在调用者和被调用者之间如何分配
- 被调用者可以直接使用哪一个寄存器有时也包括在内。(否则的话被当成ABI的细节)
- 哪一个寄存器被当作volatile的或者非volatile的,并且如果是volatile的,不需要被调用者恢复
C语言中的优化对比
下面的C程序和后面的汇编代码展示了volatile
关键字如何影响编译器的输出。这里使用的编译器是GCC。
[隐藏]汇编对照 | |
---|---|
不使用volatile | 使用volatile |
#include <stdio.h>
int main() {
int a = 10, b = 100, c = 0, d = 0;
printf("%d", a + b);
a = b;
c = b;
d = b;
printf("%d", c + d);
return 0;
}
|
#include <stdio.h>
int main() {
volatile int a = 10, b = 100, c = 0, d = 0;
printf("%d", a + b);
a = b;
c = b;
d = b;
printf("%d", c + d);
return 0;
}
|
gcc -O3 -S without.c -o without.s | gcc -S with.c -o with.s |
.file "without.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
movl $110, 4(%esp)
movl $.LC0, (%esp)
call printf
movl $200, 4(%esp)
movl $.LC0, (%esp)
call printf
addl $20, %esp
xorl %eax, %eax
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.2.1 20070719 [FreeBSD]"
|
.file "with.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $36, %esp
movl $10, -8(%ebp)
movl $100, -12(%ebp)
movl $0, -16(%ebp)
movl $0, -20(%ebp)
movl -8(%ebp), %edx
movl -12(%ebp), %eax
movl $.LC0, (%esp)
addl %edx, %eax
movl %eax, 4(%esp)
call printf
movl -12(%ebp), %eax
movl %eax, -8(%ebp)
movl -12(%ebp), %eax
movl %eax, -16(%ebp)
movl -12(%ebp), %eax
movl %eax, -20(%ebp)
movl -16(%ebp), %edx
movl -20(%ebp), %eax
movl $.LC0, (%esp)
addl %edx, %eax
movl %eax, 4(%esp)
call printf
addl $36, %esp
xorl %eax, %eax
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.2.1 20070719 [FreeBSD]"
|
Java中的volatile
Java也支持 volatile
关键字,但它被用于其他不同的用途。当 volatile
用于一个作用域时,Java保证如下:
- (适用于Java所有版本)读和写一个
volatile
变量有全局的排序。也就是说每个线程访问一个volatile
作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。(但是并不保证经常读写volatile
作用域时读和写的相对顺序,也就是说通常这并不是有用的线程构建)。 - (适用于Java5及其之后的版本)
volatile
的读和写创建了一个happens-before关系,类似于申请和释放一个互斥锁[8]。
使用volatile
会比使用锁更快,但是在一些情况下它不能工作。volatile
使用范围在Java5中得到了扩展,特别是双重检查锁定现在能够正确工作[9]。
Ada中的volatile
在Ada中,比起关键字,Volatile
标记更像是一种指令。“对于volatile对象而言,所有读和更新都会作为一个整体直接执行到内存”[10]。