NASM网际编译器手册(九)

第八章 写32位代码(Unix,Win32,DJGPP)


本章试途说明:在写能运行在Win32或Unix下或由象DJGPP Unix形式的C代码编译器,的32位代码时的一个相关
内容。它包含了如何用汇编写32位C程序的接口,如何为共享库写位置独立代码。
很多32位代码,那些能在Win32,DJGPP或PC Unix变体平台下运行的,都是运行在平坦内存模式上。这意味段
寄存器和分页机制将给你32位的4Gb地址空间,而不用担心你在用哪个相关段,而且你也应该完全忽略所有的段
寄存器。当写平坦模式的应用代码时,你永远不需要重载或修改任何段寄存器,你可以在相同的地址空间将代
码地址做为CALL和JMP的参烽你可以通过数据段地址访问你的变量,用堆栈段地址访问你的本地变量和过程参
数。每一个地址是32位的并只包含偏移部分。


8.1 32位C程序的接口
在第7.4节中很多关于16位C程序的讨论也适应于32位的工作中。内存模型的缺乏和段限制使事情变得简单。


8.1.1 外部符号名
许多32位C编译器都共享16位编译器的限制,所有全局符号(函数或数据)名字的定义格式象C程序中一样在名字前
加一个前缀下划线。然而,并不是所有都是这样:ELF规则为C符号在它们的汇编语言名前可以没有前导下划线。旧的Linux a.out C编译器,所有的Win32编译器,DJGPP和NetBSD和FreeBSD都用前导下划线;对于这些编译器,在第7.4.1节给出的宏cextern和cglobal将仍然有效。对于ELF前导下划线不应该使用。


8.1.2 函数定义和函数调用
在32位程序中的C调用限制如下所示。在下面的描述中,调用者和被调用者表示执行调用的函数和被调用的函数
调用者将函数的参数一个接一个按相反的顺序压入堆栈(从右到左,所以第一个参数将最后压入堆栈)。
调用 者执行一个CALL指令将控制传给被调用者。
被调用者返回控制,并(虽然这不需要,在函数里不必访问它们的参数)开始将ESP存入EBP使EBP做为
一个基址来从堆栈中找出它们的参数。然而调用者也可以这样做,所以调用限制为EBP必须被任何的C函数保存
。因此被调用者,如果它做为一个frame指针来设置EBP,必须先将前一个值压入堆栈。
调用者可以根据EBP来访问它们的参数。在EBP被压入堆栈时,[EBP]中将保存的EBP以前的值;[EBP+4]中为返回
地址,通过CALL隐式压入。这个参数将在[EBP+8]开始。函数最左边的参数将最后被压入堆栈,并可以从EBP中
访问这个偏移;其它的将会成功的大于这个偏移。因此,在一个象printf这样带一系列可变参数的函数里,以相
反顺序压入堆栈意味着函数知道在哪里找到它的第一个参数,并从这个参数中得到剩余参数的个数和类型。
被调用者可能希望在以后可以减小ESP,以为本地变量在堆栈中分配空间,这个可以访问EBP中的负偏移。
被调用者,如果它希望从调用者那里返回一个值,应该将值保存在AL,AX或EAX,具体哪个寄存器取决于值的
尺寸。浮点数的结果将返回在ST0。一旦被调用者完成处理,如果它分配了本地堆栈空间,它将从EBP中恢复
ESP,然后将EBP以前的值弹出堆栈,并通过RET返回(也可用RETN)。
当调用者从被调用者那里重新得到控制,函数的参数将仍在堆栈上,所以为了移去它们(不是执行一系列的POP
指令)将会加一个立即常数到ESP上。因此如果一个函数被错误数量的参数将会引起一个原型不匹配的警告,因
为调用者知道有多少个参数被压入堆栈或移除,堆栈将返回一个敏感的状态。对于用Windows API调用的
Win32程序将是一个可选择的调用限制,并象Window过程一样被Windows API调用:它们将跟在微软的_stdcall
限制后。这与Pascal限制有些相似,被调用者将通过传一个参数给RET指定来清除堆栈。然而,参数仍然以从右
到左的顺序压入堆栈。因此你可以用下面的方式定义一个C形式的函数:
global _myfunc
_myfunc: push ebp
mov ebp,esp
sub esp,0x40 ;64字节的本地堆栈空间
mov ebx,[ebp+8] ;函数的第一个参数]
;一些代码
leave ;mov esp,ebx/pop ebp
ret
在一个进程结束时,为了从你的汇编代码中调用一个C函数,你应该象下面这样写:
extern _printf
;更多下面
push dword [myint] ;我的整数变量中的一个
push dword mystring ;指向我的数据段
call _printf
add esp,byte 8
segment _DATA ;\'byte\'存储空间
myint dd 1234
mystring db \'This number->%d<-should be 1234\',10,0
这段代码将与下面的C代码等价:
int myint=1234;
printf("This number->%d<-shoulkd be 1234\\n",myint);


8.1.3 访问数据项
为了得到C变量的内容或定义一个C可以访问的变量,你只须将名字做为GLOBAL或EXTERN来定义。(名字象第
8.1.1节中一样要带前导下划线)。因此,一个C变量int i用下面代码可以被访问:
exern _i
mov eax,[_i]
如果定义你自己的整型变量使它们可以被C程序访问应该用extern int j,你可以这样做(如果需要确信你是在_DATA
段中写代码):
global _j
_j dd 0
为了访问一个C数组,你必须知道数组元素的大小。例如,int变量为4个字节长,所以如果一个C程序定义一个象
int a[10]的数组,你可以用代码mov ax,[_a+12]来访问a[3]。(字节偏移12是用数组索引3乘上数组元素的尺寸4得到
的)。32位编译器的C基本类型尺寸为:char为1,short为2,int,long,short为4,double为8。对于32位地址的指针也为
4个字节。为了访问一个C数据结构,你需要知道从结构的基地址到你想要的域的偏移。你也可以通过将C结构
定义转换成NASM里定义的结构(用STRUC), 或者用上面内容计算一个偏移。为了能做到这些,你应该读一个你
的C编译手册来找出它是如何组织数据结构的。NASM在它自己的STRUC宏不指定结构的对齐格式。所以你必须
自己指定对齐格式。你会发现下面的结构。
struct {
char c;
int i;
}foo;
为8个字节长而不是5个,因为int域为4个字节对齐的。然而,这种特性在C编译器是一个可配置的选项,也可以
用命令行参数#pargma,拟你必须找出你的编译器是如何做的。


8.1.4 c32.mac:32位C接口的帮助宏
在NASM文档的misc目录下有一个宏文件c32.mac。它定义了三个宏:proc,arg和endproc。这对C形式的过程定义
有用处,它们自动对保存调用限制的轨迹做很多工作。一个用宏来设置函数的例子如下:
proc _proc32
%$i arg
%$j arg
mov eax,[ebp+%$i]
mov ebx,[ebx+%$j]
add eax,[ebx]
endproc
这里定义了_proc32为一个带2个参数的过程,第一个(i)为一个整数而第二个(j)为一个指向整数的指针。它返回
i+*j。注意arg宏会将它的第一行扩展成一个EQU。因为宏前的标号将调用宏扩展的第一行,EQU的工作为定义
%$i为从BP中得到的一个偏移。一个相关本地变量被使用,本地的通过proc宏来压入堆栈 并且通过endproc宏
来弹出堆栈。所以相同名字的参数将在后面的过程中被使用。当然,你不必那样做。arg可以带一个可选的参
数,来给出参数的尺寸。如果尺寸没有给出则为4,因为许多参数都为int或指针。


8.2 写NetBSD/FreeBSD/OpenBSD和Linux/ELF共享库
ELF将替换Linux下老的a.out格式,这是因为它支持位置独立代码(PIC),这可以使写共享库变得容易些。NASM支
持ELF的位置独立代码特性,所以你可以在NASM里写Linux的ELF共享库。NetBSD,与它的近亲FreeBS和OpenBSD
在支持a.out格式的PIC方面采用不同方法。NASM做为aoutb输出格式支持这个,所以你也可以用NASM写一个BSD
共享库。操作系统通过内存映象文件写一个PIC共享库。在运行进程的地址空间的任何附近的点。库代码段内容
必须与它在内存中哪个地方装载无关。然而 ,你不能用下面的代码定义你的一个变量 :
mov eax,[myvar] ;错误
相反,连接器提供了一个叫全局偏移表的内存空间,或GOT;GOT被放在从你库代码一个常数距离的位置,所以
你可以找出你的库是在哪个地方装载的(用CALL和POP连合),你可以得到GOT的地址,你可以在GOT中连接器入
口外装载变量。一个PIC共享库的数据段没有这些限制:由于数据段是可写的,它必须拷贝到内存中而不是从
库文件中分页,一旦它被重定位也需要拷贝。所以你可以将重定位序号类型放在在数据段中而不用担心。(关于
一个警告见第8.,2.4节)。


8.2.1 取得GOT的地址
你共享库中的每一个代码模块,应该做为一个外部符号定义GOT:
extern _GLOBAL_OFFSET_TABLE_ ;在ELF中
extern _GLOBAL_OFFSET_TABLE_ ;在BSD a.out
在你共享库中任何函数的开始将计划访问你的数据或BSS段,你必须计算GOT的地址。这个可以通过下面的格式
写函数:
func: push ebp
mov ebp,esp
push ebx
call .get_GOT
.get_GOT:pop ebx
add ebx,_GOBAL_OFFSET_TABLE_+$$-.get_GOT wrt ..gotpc
;函数主体
mov ebx,[ebp-4]
mov esp,ebp
pop ebp
ret
(对于BSD,符号_GLOBAL_OFFSET_TABLE需要二个前导下划线。)函数的前两行为简单的标准C序言来设置一个
stack frame,最后三行为标准C函数尾声。第三行和第四行到最后一行将保存并恢复EBX寄存器,因为PIC共享库
用这个寄存器存储和恢复GOT的地址。最有兴趣的位为CALL指令和后面的两行。CALL和POP合起来将得到标号
.get_GOT的地址而不用知道程序是在哪个地方装载的(CALL指令用当前位置来编码)。ADD指令将利用指定PIC重定
位类型的一个:GOTPC重定位。用WRT .gotpc指定限定,引用的符号(这个_GLOBAL_OFFSET_TABLE_,指定的符号将
被指定为GOT)将从段开始给出一个偏移。(事实上,ELF将它编码为ADD指令操作符域的偏移,但NASM将故意简
化它,所以你可以对ELF和BSD做相同的事。)所以指令将加段开始的值,来得到GOT的真实地址,并且减去
.get_GOT从EBX得到的值。然而,当指令完成时,EBX包含GOT的地址。如果你不象上面的,也不用担心:它将
永远需要得到GOT地址,所以你可以将那三个指令放到一个宏中并安全的忽略它们:
%macro get_GOT 0
call %%getgot
%%getgot:pop ebx
add ebx,_GLOBAL_OFFSET_TABLE_+$$-%%getgot wrt ..gotpc
%endmacro


8.,2.2 找出你本地的数据项
当得到GOT,你也可以用它得到你数据项的地址 。许多变量可以放在你定义的段中;它们可以用..gotoff指定WRT
类型来访问。如下:
lea eax,[ebx+myvar wrt ..gotoff]
表达式myvar wrt ..gotoff被计算,当共享库被连接时,将会成为从GOT开始的本地变量myvar的偏移。然而,将它
加到一EBX上将在EAX中入入一个myvar的真正地址。
如果你不指定它们的尺寸而用GLOBAL定义变量时,它们将在库中的代码模块间共享,但不能从库引用到装载它
的程序。它们将仍在你的原始数据和BSS段中,所以你可以与本地变量一样的方法访问它,用上面的..gotoff
机制。注意由于BSD a.out格式处理这种重定位类型的特性,在你访问的地址上必须有至少一个非本地符在相同
的段中。


8.2.3 找出外部和常用数据项
如果你的库需要得到一个外部变量(在库的外部,而不是它里面的一个模块),你必须用..got类型来得到它。..got
类型取代给出唑GOT基址到变量的偏移,而是给出从GOT基址到一个包含变量地址的GOT入口的偏移。连接器在
它构造库时将设置这个GOT入口,动态连接器将在装载时放置正确的地址在里面。所以在EAX里得到外部变量
extvar的地址,你应该如下:
mov eax,[ebx+extvar wrt ..got]
这将在GOT入口外装载extvar的地址。连接器,当它构造共享库时,将每个类型..got的重定位收集到一起,然后
构造GOT以保证它有每一个需要的入口位置。常用变量必须这样来访问。


8.2.4 引出符号给库用户
如果你想将符号引出给库用户,你必须定义它们是函数还是数据,如果它们是数据,你必须给出数据项的尺寸
。这是因为动态连接器必须为引出函数构造过程连接表入口,并也可以从它们定义的库数据段中移去外部数据
项。所以导出一个函数到库用户,你必须用:
global func:functioin ;定义它为一个函数
func: push ebp
;等等
也可以导出一个象数组的数据项,你必须写一个全局数组:data array.end-arrya;也给出尺寸。
array: resd 128
.end:
注意:如果你导出一个变量到库用户,通过用GLOBAL定义并且提供一个尺寸,变量将在主程序中的一个数据段
结束,而不是在你定义的库数据段,所以你必须用..got机制来访问全局变量而不是用..gotoff,就象它是外部的。
(它将会有效的)。相等的,如果你需要在你的数据段中的一个存储一个导出全局符号的地址,你不能用下面的
代码实现:
dataptr: dd global_data_item ;错误
NASM将做为一个序号重定位来解释,global_data_item将是一个从.data段开始的偏移(什么都可以),所以这将
引用你的数据段结束点而不是外部符号在别的地方。上面的代码将写成:
dataptr: dd global_data_item wrt ..sym
这将利用指定的WRT类型 ..sym来使NASM为找一个特殊符号而搜索符号表,而不是用段基址重定位。
每一种方法都将为函数工作:引用你函数中的一个:
funcptr: dd my_function
将给用户一个你写代码的地址,而
funcptr: dd my_function wrt ..sym
将给出一个函数的过程连接表的地址, 这将调用程序相信函数的存在。每一个地址都可以有效的调用函数。


8.2.5 在库外面调用过程
在你的共享库外面调用过程必须用一个过程连接表或PLT来做。PLT被放在库装载时的一个已知的偏移,所以库
代码可以做一个位置独立代码来调用PLT。在PLT中代码将跳过在GOT中的偏移,所以函数调用其它共享库或在
主程序中的例程可以传给它们真正的目标。为了调用一个外部例程,你必须用另外一个指点定的PIC重定位类
型,WRT ..plt。这将比GOT-基位置容易:你可以简单的用PLT-相关版本CALL printf WRT ..plt代替CALL printf。


8.2.6 生成库文件
写一些代码模块并将它们汇编成.o文件,然后用下面的命令行生成你的共享库文件:
ld -shared -o library.so module1.o module2.o #对于ELF
ld -Bshareable -o library.so module1.o module2.o #对于BSD
对于ELF,如果你的共享库是在系统的子目录中如/usr/lib或/lib,它将会用-soname标志给连接器,来存最终文件的
名字。并用一个库中的版本号:
ld -shared -soname library.so.1 -o library.so.1.2 *.o
你也可以将library.so.1.2拷贝到库目录中,并创建library.so.1做为一个符号连接。

原文地址:https://www.cnblogs.com/cnlmjer/p/4099878.html