Native编程知识集锦
不同编译器(甚至同一款编译器的不同版本)创建的目标文件(.o/.obj)、静态库常常不能相互链接,因此常常需要获得源码,从头编译。
动态链接库(DLL)的互操作性则较好,如果DLL是基于C编写的,那么互操作性通常不是问题。即使DLL基于C++编写, 只要通过C接口( extern C )调用,通常也没有问题。例如MinGW就可以正常的链接到Windows的C运行时库。
互操作性问题的根源不仅仅是C++名称改编,更关键的是ABI的不兼容性。因此即使解决了名称改编问题,让链接阶段正常完成,也可能在运行时,调用DLL时出现程序崩溃。ABI的不兼容性可以表现在:
- 数据类型的大小、对齐方式。对齐方式一致性可能需要正确的编译器设置(例如 -mms-bitfields )
- 调用约定:如何传递参数、如何接受返回值。例如所有参数通过栈传递,还是部分通过寄存器传递;寄存器和参数的对应关系;栈传递的第一个参数位于栈底还是栈顶
- 内存模型的不一致性:例如MSVC DLL中的 new/delete、malloc/free 和Cygwin newlib中对应函数不能互操作
- 异常处理模型的不一致性:MSVC DLL抛出的异常不能被Cygwin创建的exe文件捕获,反之亦然
- 旧的GNU SJLJ异常模型(GCC 3.x-)可以和MSVC++的异常模型兼容;而新的DWARF2则不能兼容
使用GCC时,可用的异常处理机制(exception handling,EH)包括:
- DWARF (DW2, dwarf-2):该机制不能在64bit的Windows下运行。在32bit的Windows下,异常也不能通过任何非DW2感知的代码传播(propagate )。因此,Windows系统DLL、Visual Studio构建的DLL,都不能和此EH联用
- SJLJ (setjmp/longjmp):大部分情况下同时支持32/64bit的Windows,但是该EH不是Zero-cost的,相对之下较慢
- SEH (Structured Exception Handling):这是微软的EH,GCC从4.8开始支持它
一个现代编译器的主要工作流程:
把源文件编译成中间代码文件(.o,或者Windows的.obj)文件的过程。通常每个源文件都应该对应于一个中间目标文件。编译阶段,检查语法错误、函数、变量的声明是否正确(需要告知编译器头文件所在位置)。
把多个中间代码文件合并为一个可执行文件的过程。连接器在.o文件中找到函数的实现,解析未定义的符号引用,将.o文件中的占位符替换为符号的地址。链接分为静态链接、动态链接两种,对于前者,依赖库仅需要在链接期可见;对于后者,依赖库在链接期、运行期都必须可见。
如果源文件太多,则导致有大量的.o文件,链接时需要逐一指出其文件名,这很不方便。库文件(.a,或者Window的.lib)文件相当于把.o文件打包,库文件是预先编译好的函数的集合。
在Linux中,库文件的命名必须遵守格式: lib[libname].[a|so] 。也就是说,库文件必须以lib三个字母开头,其后跟着库的名称,最后是说明库类型的扩展名:
- *.a表示传统的静态库,又称归档文件(archive),静态库只是目标文件的简单打包,使用ar命令,可以将多个*.o文件打包为一个静态库
- *.so表示共享库。共享库的出现是为了解决多任务系统中静态资源浪费资源的问题:当多个程序同时静态链接一个静态库时,会在内存中出现同一函数的多份副本,静态链接的可执行文件体积也较大
传统的非共享的“静态”库,其代码段会在链接阶段直接加入到目标可执行文件的代码段中,这意味着如果两个可执行程序依赖于同一个静态库,那么静态库的代码将在内存中出现两次,这会造成浪费。而共享库的代码段则独立存放一份在内存中,程序运行时,由操作系统负责绑定调用到真实的共享库函数地址。虽然共享库一般都延迟到其加载后才链接到程序,但是静态链接也是支持的。
共享库在不同的操作系统中实现的方式不同,但是基本上都使用与可执行文件(Executables)一样的格式。这样做一方面仅需要一个加载器;另一方面则可以把可执行文件当做共享库使用(只要其具有符号表, symbol table)。典型的文件格式,在Windows上是PE(Portable Executable);在Linux上是 ELF(Executable and Linkable Format)。
在Linux中,当一个程序使用共享库,其链接方式如下:
- 程序本身不再包含函数代码,而是引用运行时可访问的共享代码
- 当编译好的程序载入内存并执行时,上述引用被解析,并产生对共享库的调用,如果有必要共享库才加载到内存
共享库具有以下优点:
- 内存中可以只保留一份共享库的副本供所有程序使用;磁盘上也只需要保留一份
- 共享库可以独立于依赖它的应用程序进行更新。如果程序依赖libm.so,那么系统中可能存在libm的第6个修订版:/lib/libm.so.6,只需要将/lib/libm.so设置为前者的符号链接,即可改变使用的链接库的版本
在Linux中,负责加载动态库,并解析应用程序引用的程序是ld.so(或者其它名称),用于搜索动态库的额外位置可以通过配置文件:/etc/ld.so.conf指定,如果修改了该文件,需要调用ldconfig命令。
所谓交叉编译,就是指编译出在其它体系结构(CPU指令集架构)上运行的程序,比如在x86平台上编译出ARM平台的程序,这种编译方式在异平台移植和嵌入式开发时使用的很普遍。相对于交叉编译的叫本地编译。
用来进行交叉编译的编译器称为交叉编译器,为了不和本地编译器混淆,交叉编译器通常具有特定的前缀,来指明它编译的目标体系结构。
使用GCC交叉编译器时,方式与使用本地编译器差不多,但是必须用-L和-I参数指定编译器使用的目标体系结构的库和头文件。
函数命名约定、名称改编规则都属于应用程序二进制接口(Application binary interface,ABI)的组成部分。不同的编译器实现具有微妙的区别,另一方面,各编译器对用作API standard (例如 stdcall) 的实现则相当统一。
在x86架构处理器上进行编程时,存在多种可用的函数调用约定,他们规定了:
- 原子(标量)参数,或者复杂参数的每个部分,其被分配(allocated)的顺序
- 参数如何传递(压栈,或者存放到寄存器,或者两者的混合)
- 被调用函数必须为调用者保留哪些寄存器
- 准备栈、清理栈的工作如何在调用者和被调用函数之间进行分配
下表是常见的调用约定
调用约定 | 说明 |
stdcall | __stdcal 参数传递:从右到左压栈 栈清理:被调用函数在返回前清理栈(不支持可变长参数) Win32 API均是使用该调用方式 |
fastcall | __fastcall 参数传递:左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECX和EDX寄存器,其余参数从右到左压栈 栈清理:被调用函数在返回前清理栈 微软的一种调用约定 |
cdecl | __cdecl 参数传递:从右到左压栈 栈清理:调用者负责弹出参数以清理栈(支持可变长参数) cdecl(C declaration)是多种x86架构下C语言编译器使用的调用约定。整数和内存地址通过EAX寄存器返回,浮点数则通过ST0寄存器返回。调用者保留EAX、ECX、EDX寄存器,其余寄存器由被调用函数保留。在调用一个函数前,浮点寄存器ST0至ST7必须为空(弹出或释放);退出一个函数时ST1至ST7必须为空 |
函数名称改编对代码中符号名称到连接器使用的符号名称的映射规则进行约定。现代编程语言提供的诸如函数重载的功能,使单纯使用函数的名称无法进行唯一性鉴别,因此必须按照一定的规则进行函数名改写,以便连接(连接器必须知道函数的名称、参数个数、类型等信息)。
由于C++符号会通过DLL、SO文件导出,供其它人使用,因此名称改编不是单个编译器的内部事务了,不同编译器,甚至同一种编译器的不同版本在进行链接时,经常会出现unresolved externals错误。
改编后的名称,存在于obj文件中,链接阶段将从obj中读取这些名称。
这些规则用微软发起,其它编译器(例如: Borland、GNU GCC)在Windows下编译代码时也遵守,甚至其它语言(例如: Delphi、Fortran、C#)也遵守。因此不同语言编写的子例程,可以与使用不同调用约定的Windows库函数相互调用。
调用约定 | func(int x)的名称改编结果(32bit编译器) |
_cdecl | _func |
_stdcall | _func@4 后缀的数字是参数列表的字节数 |
_fastcall | @func@4 后缀的数字是参数列表的字节数,包括传递给寄存器的 |
注意:Microsoft C的64bit编译器没有前导的下划线,可能导致某些情况下出现unresolved externals错误
C++编译器是名称改编最主要的使用者,为了与C的兼容性,C++编译后的符号名称必须服从C的标识符规则。
由于C++语言的复杂性(类、模板、名字空间、操作符重载)、缺乏标准,导致C++名称改编非常混乱复杂,很少有连接器能够连接其他编译器的obj代码。
下表显示多种编译器的改编结果
Compiler | void h(int) | void h(int, char) | void h(void) |
GCC 3.x and 4.x | _Z1hi | _Z1hic | _Z1hv |
GCC 2.9x | h__Fi | h__Fic | h__Fv |
Microsoft Visual C++ v6-v10 | ?h@@YAXH@Z | ?h@@YAXHD@Z | ?h@@YAXXZ |
Digital Mars C++ | ?h@@YAXH@Z | ?h@@YAXHD@Z | ?h@@YAXXZ |
Borland C++ v3.1 | @h$qi | @h$qizc | @h$qv |
Tru64 C++ V6.5 (ARM mode) | h__Xi | h__Xic | h__Xv |
Tru64 C++ V6.5 (ANSI mode) | __7h__Fi | __7h__Fic | __7h__Fv |
1 2 3 4 5 6 7 |
#ifdef __cplusplus extern "C" { #endif /* 在这里的代码不会进行名称改编 */ #ifdef __cplusplus } #endif |
ABI(application binary interface)指两个程序模块之间的接口,通常其中一个是库、操作系统提供的服务,另一个是用户执行的应用程序。ABI定义了及其代码如何访问数据结构和程序,想当低级且依赖于硬件。
ABI涵盖以下方面的细节:
- 数据类型的大小、布局和对齐
- 调用约定(控制着函数的参数如何传送以及如何接受返回值)—— 通过栈还是寄存器传递,通过栈传递的时候,首先传递第一个还是最后一个参数
- 系统调用的编码、应用如何向操作系统进行系统调用
- 目标文件的二进制格式
一些ABI标准化了一些细节,例如C++名称改编。
目标文件是源代码编译后未进行链接的中间文件(.o/.obj),和可执行文件(ELF/PE)的结构和内容相似。
常见的可执行文件格式主要有 Windows 的 PE(Portable Executable)和 Linux 的 ELF(Executable and Linkable Format)。两者都是通用目标文件格式(COFF,Common Object File Format)的变体。在Windows 下目标文件文件(COFF文件)和可执行文件(PE文件)统称为 PE-COFF 文件,Linux 下则统称为 ELF 文件。
COFF 是由System V Release 3 首次提出并使用的格式规范,Microsoft 在其基础上,制定了 PE格式标准。System V Release 4 在 COFF 的基础上引入了ELF 格式,Linux 系统也是以 ELF 作为基本的可执行文件格式。
动态链接库、静态链接库也都按照可执行文件的格式进行存储。
一个ELF文件由以下部分组成:
- ELF头
- 定义了使用32bit还是64bit地址
- 文件数据(file data),包含:
- 程序头表(Program header table):描述一个或多个段(segments)
- 分区头表(Section header table):描述一个或多个分区(sections)
- 各种段 —— 程序在执行时所必须的信息
- .text 程序需要执行的指令
- .data 固定到程序镜像中的初始化数据
- .rodata 只读的数据
- .bss 固定到程序镜像中的未初始化数据。系统在程序运行时将这些数据初始化为0
- .rel.text、 .rel.data、 .rel.rodata 文本、数据段的重定向(relocation)信息
- .symtab 符号表
- .strtab 字符串
- .init 进程初始化代码的指令
- .fini 进程终止代码的指令
- .debug 符号化的调试信息
- .line 符号化调试信息的行号信息,描述程序源代码和机器码之间的对应关系
- .comment 存储其它额外信息
- 各种分区 —— 链接和重定位所必须的信息
Linux下的ELF文件分为以下几个子类:
类型 | 说明 |
可重定位文件/Relocatable File |
如目标文件与静态链接库。这种文件包含代码与数据,可以用来连接成可执行文件或共享目标文件 这种文件是连接器的输入,所谓可重定位,是因为这种文件中的函数以及其它符号,仍然存储的是名字,而非地址 |
共享目标文件/Shared Object File |
代码和数据,主要有两种用途
|
可执行文件/Executable File | 可直接执行的程序 |
核心转储文件/Core Dump File | 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 |
宏 | 说明 |
__DATE__ | 当前源代码编译的日期,格式 Mmm dd yyyy |
__TIME__ | 当前源文件的编译时间,格式 hh:mm:ss |
__TIMESTAMP__ | 当前源文件的编译时间戳 |
__FILE__ | 当前源文件的名称 |
__LINE__ | 当前宏所在行号 |
_CHAR_UNSIGNED | 默认char是否无符号 |
__cplusplus | 用于定义一段专属于C++的内容 |
_CPPRTTI | 启用C++运行时类型识别 |
_CPPUNWIND | 启用C++异常处理 |
_DEBUG | 表示当前使用调试的C运行时库,或者创建调试版本的DLL |
_DLL | 表示与DLL版本的C运行时库链接 |
__FUNCTION__ | 仅在函数内使用,函数名称 |
__FUNCDNAME__ | 仅在函数内使用,改变后的函数名称 |
_FUNCSIG__ | 仅在函数内使用,函数签名 |
_INTEGRAL_MAX_BITS | 整数最大位数 |
_WIN32 | 对于Win32/Win64应用,总是定义 |
_WIN64 | 对于Win64程序,定义 |
_UNICODE |
控制C运行时库/MFC头文件中和字符集有关宏的处理,如果定义了该宏:
|
UNICODE |
控制Windows头文件中和字符集有关的宏的指向,如果定义了该宏:
|
Leave a Reply