C 中near far 指针 及相关函数

1、64K限制的困扰

 

我们考虑下面一段16位C语言代码片断:

char g_c;

void func (char *p)

{

 char c;

 

 p = &c;

 p = &g_c;

 p = (char *)malloc(10 * sizeof(char));

 free(p);

 

 return;

}

显然,g_c是全局变量,按照教科书的说法,它应该放在全局变量区,c是局部变量,因此c放在栈区,而malloc所申请的空间应该是在堆区。上面的代码从语义上讲没有半点错误,一个指针p可以指向全局变量区的内存,也可以指向堆区或栈区的内存。

大家知道,16Bit C语言的指针是16位的,因此只能访问64K大小的空间,然而我们使用指针对全局变量,堆,栈进行寻址的时候,都没有指定一个数据段。那么也就是说,全局变量,堆以及栈都是在同一个段中的,这三部分的大小加起来不可能大于64K,这也是malloc的参数int类型的最大表示范围。

按照我的理解这64K应该是这样的:

64K最顶端是栈区(因为栈总是从上往下的),一般我们在程序的开始会设置SS的内容和DS一样,SP的值为0,如果有进栈操作的话,SP会减少2,而变成从FFFE,然后往下减。

64K最底端是数据区(这部分是固定的,堆和栈是可变的),我们声明了Data Segment之后,一般会将Data Segment的段起始地址给DS,然而,Data Segment中的变量是按顺序从低往高排的,一旦编译连接成可执行文件之后,这部分内容的大小(或称为长度)是不会变的。我们可以更改的是堆和栈的大小。

堆区位于数据区以上部分,也就是如果堆区的不断增长,或者栈区的不断进栈,最终可能造成堆区和栈区交叉,也就是我们俗称的堆栈溢出。

或许会有些疑问,认为我将局部变量放在栈区是不对的,这里就这部分内容再展开讨论一下,对于函数的参数是放在栈区的,大家不会有所怀疑,因为函数调用除了使用寄存器传递参数之外只能使用堆栈传递了(当然使用全局变量也可以,不过这里只讨论形参与实参的传递),因此调用Func的过程伪代码如下所示:

 lea bx, p

 push bx

 call func

 pop bx

而对于局部变量c,func又应该是如何做的呢?

func PROC NEAR

 push bp

 mov bp, sp

 sub sp, 2

;;;[bp-2]就是c的地址了,[bp+4]应该是实参p的地址。

 

...

 pop bp

 ret

func ENDP

p的地址是bp+4应该没有什么疑问,前面有一个Push bp,占用了两个字节的栈空间,然后还有调用者的代码偏移地址(函数声明为NEAR,栈中只有OFFSET,没有SEGMENT)。

对于com类型的可执行文件来讲,数据区嵌入在代码区中的,代码区和数据区合并在一起。

那么也就使说我们在程序中能使用的数据空间只有64K大小,还要扣除函数调用使用的栈区。当然这个空间对很大一部分程序来说是足够的,至少在我上大学写的大部分数据结构和算法的程序来说(不过后来都转向Windows的Visual C++了,那是32Bit的代码,内存空间没有64K限制)。然而如果是对一些大数据量的处理呢?这个64K空间可能就比较小了。我的缓冲区空间可能就不能满足庞大的吞吐量要求。

在以前一键恢复(FREST)项目中,我们完成的功能是对磁盘分区的备份,这里涉及到大量大块数据的处理,包括FAT表,FAT数据区等,如果考虑上面的64K限制,我们的读写缓冲区就只有很小,而且还要考虑堆栈的溢出可能。幸好我们使用的是汇编语言,它允许我们直接操纵内存,而且还提供了除DS外的ES,FS,GS这几个段寄存器,于是我们可以设置DS作为数据段,而ES,FS作为读写缓冲区段,直接指定ES和FS的地址(这里要说明的是一键恢复软件是NoneOS的软件,没有运行在任何操作系统上,因此如果有操作系统支持的软件请考虑操作系统内存的分配情况)。而且在处理庞大的FAT表时,使用保护模式的短限制,我们将GS段设置为4G段大小,这样,可以通过GS访问整个4G内存地址,将FAT表拷贝到1M以上空间有利于我们对FAT表的解析。

这个项目已经结束多时了,曾经有想过用C语言重写部分代码,但是一直苦恼于64K的段限制而迟迟没有做,抛开这整个内核而单单只重写菜单部分就没什么重大意义了。

这几天,突有想法,对C语言FAR指针的使用进行了一些试验,不敢独享,将这部分心得公开予大家,也希望有什么做的不对或理解不到的地方、问题或建议可以与我交流分享:luo_leo@hotmail.com

 

2、FAR指针作为参数传递和返回值

 

OK,前面讲了那么多废话,下面我们开始进入正题了。

C语言中正常指针是16位的,FAR指针应该是32位的吧,那么这个32位的指针在参数中传递和返回值时是如何进行的呢?带着这个疑问开始我们的一系列探索。下面的试验中,汇编编译器使用MASM,C语言编译器使用TC中的TCC,连接器使用MASM配套的LINK。

16Bit C语言函数的返回值放在AX中,而返回FAR指针应该是32BIT,毫无疑问,这32BIT的低16位应该是返回在AX中,我们要看看那高16BIT返回在那里。

为了省去C语言中初始化那么多细节,我主函数采用汇编来写,然后调用C函数,C代码如下:

char far * p;

char far * func1(int size)

{

 return p;

}

这是最简单的FAR指针返回值了,OK,编译完成之后使用TD跟踪查看一下这个函数生成的汇编代码:

 push bp

 mov bp, sp

 

 mov dx, [016E]

 mov ax, [016C]

 jmp 015A

 

015A: pop bp

 ret

 

噢,原来返回值高16BIT在DX中。这样我们在使用汇编调用返回FAR指针的C函数时就必须使用DX和AX两个寄存器作为返回值的存放。

接下来我们看看FAR指针作为参数的情况。

extern void afree(char far *p);

void lfree(char far * p)

{

 afree(p);

}

lfree对应汇编代码如下所示:

 push bp

 mov bp, sp

 push word ptr [bp+6]

 push word ptr [bp+4]

 call 012C

 pop cx

 pop cx

 pop bp

 ret

显然,FAR指针是作为一个32BIT的参数在堆栈中传递,因此需要在堆栈中做两次Push Word的操作。

慢着,我们只看到进行Call操作的汇编代码,如果在MASM中要用Invoke来调用呢?因为invoke具有和C语言函数声明和调用类似格式的,因此在汇编代码我比较偏向喜欢用invoke调用C语言函数。

因为FAR指针是32BIT的,所以而16Bit 8086汇编不支持eax之类32bit操作,在翻阅了MASM的User Manual之后终于找到FAR指针的调用方法:

首先,FAR指针是32BIT,因此我们在PROTO声明中必须声明为DWORD,或其他等价长度的类型,不能使用PTR之类的指针类型,因为这个类型只有16BIT长度。

 lfree PROTO NEAR C, p:DWORD

在调用的时候,传递参数必须借用ES来传递,如下所示:

  invoke lfree, es::bx

注意,这里的ES::BX中间必须是两个点,刚开始我还以为是User Manual写错了呢,后来发现使用ES:BX根本通不过编译,因为类型不对。 

上面啰嗦这么多,稍微总结一下:

1>FAR指针使用DX:AX做返回值。

2>调用参数为FAR指针的函数时,invoke参数使用ES::BX。

 

3、FAR指针存取

至今我们知道FAR指针的长度是32BIT,然而在FAR指针中存放的是变量的绝对地址呢,还是段地址+偏移量之类的。下面再次拿出我们分析的工具来看看,这回我们往某个地址中存值。

void far * lmalloc(int size)

{

 *p = 0x5A;

 return p;

}

然后使用TD查看一下汇编代码:

 push bp

 mov bp, sp

 les bx, [016C]

 mov es:byte ptr[bx], 5A

 mov dx, [016E]

 mov ax, [016C]

 jmp 015A

015A: pop bp

 ret

噢,原来FAR指针的高16位存放的是变量的段地址,低16位存放变量的段内偏移。 

 

4、FAR指针的局限

 

FAR指针虽然可以让我们摆脱数据区+堆区+栈区 <= 64K的限制,但是FAR指针和保护模式下32BIT指针还是有本质的区别,还是无法超越64K的段限制,也就是说假如我们把FAR指针一直往上加,过了64K边界之后,FAR指针又会回到段起始处开始再往上加。

为了验证这个结论,我们再做一个试验:

void far * lmalloc(int size)

{

 p = 0x20001000;

 *p = 0x5A;

 p = p + 0x0000FF00;

 return p;

}

它所对应的汇编代码如下:

 push bp

 mov bp, sp

 mov word ptr [017E], 2000

 mov word ptr [017C], 1000

 les bx, [017C]

 mov es:byte ptr [bx], 5A

 mov ax, [017E]

 mov dx, [017C]

 add dx, FF00

 mov [017E], ax

 mov [017C], dx

 mov dx, [017E]

 mov ax, [017C]

 jmp 016A

016A: pop bp

 ret

 

写在最后

 

什么是FAR指针,FAR指针如何使用?是一个一直困挠了我很久的问题,这里欲借几个简单的汇编和c语言函数来分析FAR指针的用法。在多种语言混合编程中,汇编和C语言可能是用的比较多的,突破跨语言的障碍有助于我们在进行系统分析的时候可以将语言的依赖性降低。

为什么选用C语言?也许是我有C语言情结吧,大学里学的第一门语言课就是C语言,而且我认为最重要的是C语言和汇编很近,用C语言可以比较容易的看清机器真正做的事情。汇编的好处是直接对应CPU执行码,但是它毕竟太麻烦,而且现在还有这么多汇编语言版本,就像GCC的内嵌汇编AT&T格式就看得我很头疼,我也还是比较偏向于MASM,呵呵。

来源:http://cpyjzhen.blog.163.com/blog/static/14293975200732754346452/

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
near指针和far指针?

在DOS下(实模式)地址是分段的,每一段的长度为64K字节,刚好是16位(二进制的十六位)。
near指针的长度是16位的,所以可指向的地址范围是64K字节,通常说near指针的寻址范围是64K。
far指针的长度是32位,含有一个16位的基地址和16位的偏移量,将基地址乘以16后再与偏移量相加,(所以实际上far指针是20位的长度。)即可得到far指针的1M字节的偏移量。所以far指针的寻址范围是1M字节,超过了一个段64K的容量。例如一个far指针的段地址为0x7000,偏移量为0x1244,则该指针指向地址0x71224.如果一个far指针的段地址是0x7122,偏移量为0x0004,则该指针也指向地址0x71224。
    如果没有指定一个指针是near或far,那么默认是near。所以far指针要显式指定。far指针工作起来要慢一些,因为每次访问一个far指针时,都要将数据段或程序段的数据交换出来。另外,far指针的运算也比较反常,例如上面讲到的far指针指向同一个地址,但是比较的结果却不相同。


什么时候使用far指针?

当使用小代码或小数据存储模式时,不能编译一个有很多代码或数据的程序。因为在64K的一个段中,不能放下所有的代码与数据。为了解决这个问题,需要指定以far函数或far指针来使用这部分的空间(64K以外的空间)。许多库函数就是显式地指定为far函数的形式。far指针通常和farmalloc()这样的内存分配函数一起使用。

http://zhidao.baidu.com/question/6731078.html?fr=ala0

 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
far是和near对应的,就是一般16位系统下程序的函数调用都在64k地址范围内的,就是16位寻址就够了,但是当代码比较庞大时,16位就可能不够了.far就代表32位寻址。一般不写默认就是near。

现在都已经不用了,都是32位系统了…
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
far即为远地址,16位模式下(如80x86系列)内存是分段寻址的;
指定void   far   fun1();的话,是说函数fun1的入口点在其它段中,调用时将用到段地址;
如果指定为near的话,入口点将在本段之内,调用时值使用段内的偏移地址。

32位中采用线性的平坦寻址方式,这一概念已被抛弃了,内存中地址统一为32位宽,大体
说来就是这样,细节参见相关资料
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值