动态分配栈内存之alloca内幕Word文件下载.docx

上传人:b****5 文档编号:18787469 上传时间:2023-01-01 格式:DOCX 页数:10 大小:21.55KB
下载 相关 举报
动态分配栈内存之alloca内幕Word文件下载.docx_第1页
第1页 / 共10页
动态分配栈内存之alloca内幕Word文件下载.docx_第2页
第2页 / 共10页
动态分配栈内存之alloca内幕Word文件下载.docx_第3页
第3页 / 共10页
动态分配栈内存之alloca内幕Word文件下载.docx_第4页
第4页 / 共10页
动态分配栈内存之alloca内幕Word文件下载.docx_第5页
第5页 / 共10页
点击查看更多>>
下载资源
资源描述

动态分配栈内存之alloca内幕Word文件下载.docx

《动态分配栈内存之alloca内幕Word文件下载.docx》由会员分享,可在线阅读,更多相关《动态分配栈内存之alloca内幕Word文件下载.docx(10页珍藏版)》请在冰豆网上搜索。

动态分配栈内存之alloca内幕Word文件下载.docx

[cpp]viewplaincopyprint?

&

lt;

P&

gt;

.xlist

includecruntime.inc

.list&

/P&

extern_chkstk:

near&

;

sizeofapageofmemory&

CODESEG&

page&

public_alloca_probe_8

_alloca_probe_16proc;

16bytealignedalloca

pushecx

leaecx,[esp]+8;

TOSbeforeenteringthisfunction

subecx,eax;

NewTOS

andecx,(16-1);

Distancefrom16bitalign(aligndown)

addeax,ecx;

Increaseallocationsize

sbbecx,ecx;

ecx=0xFFFFFFFFifsizewrappedaround

oreax,ecx;

capallocationsizeonwraparound

popecx;

Restoreecx

jmp_chkstk

alloca_8:

;

8bytealignedalloca

_alloca_probe_8=alloca_8

andecx,(8-1);

Distancefrom8bitalign(aligndown)

IncreaseallocationSize

_alloca_probe_16endp

end

默认会编译为16字节对齐的版本,仔细看一下,这里所谓的16字节对齐倒也不一定,leaecx,[esp]+8这句获得进入此函数之前的esp值并写入ecx中,这里加8的原因很明显,前4个字节是保存的ecx的值,后4个字节是函数的返回地址,加8即得到上一层函数调用本函数时的esp值,这里没有参数压栈,参数是寄存器传递的。

因此,这个ecx的值可以假设为一个定值(这个值也是至少4字节对齐的),然后下面3句汇编代码中,eax是外部传入的要开辟栈内存字节数,这个字节数始终是4字节对齐的。

那么subecx,eax这句之后的结果就可以是4字节对齐且非16字节对齐,这样一来,在andecx,(16-1)并addeax,ecx后,eax的值就是非16字节对齐的。

至于8字节对齐的版本,你可以试着推算一下会不会存在算出的eax是非8字节对齐的,这个不是难点。

在此函数里,我们发现还没有真正的开辟栈内存,因为esp(也就是前面提到的栈指针,也就是栈顶指针,上面的汇编代码中的TOS也就是栈顶:

Topofstack的意思)的值还没有减去eax(申请内存的大小)而改变。

然后我们注意到,在popecx还原ecx的值(因为此函数需要ecx来协助,因此进函数就pushecx保存,然后结束之后再pop还原)之后,还有一个jmp跳转,跳转到了_chkstk,此函数很明显,意为:

checkstack,用于检查堆栈是否溢出。

此函数通常会被编译器插入到某个开辟了一定大小函数头部,用于进入函数时进行栈内存溢出检查,例如你在一个函数中定义一个较大的数组,此时编译器会强制插入_chkstk函数进行检查(这里单指VC下,其他编译器的方式不一定一致)。

于是,到此可以猜测,这个_alloca_probe_16函数只是负责计算实际对齐后该分配多少字节的栈内存,并保存到eax中,由于_chkstk函数也会用到eax的值,这里也是通过寄存器传参的。

并且可以看出_alloca_probe_16函数和_chkstk函数联系紧密,都是直接jmp过去的。

好了,来看看_chkstk函数吧,此函数位于之前的目录下,也是一个汇编源文件:

chkstk.asm。

_PAGESIZE_equ1000h&

public_alloca_probe

_chkstkproc

_alloca_probe=_chkstk

CalculatenewTOS.

leaecx,[esp]+8-4;

TOSbeforeenteringfunction+sizeforretvalue

newTOS

Handleallocationsizethatresultsinwraparound.

WraparoundwillresultinStackOverflowexception.

sbbeax,eax;

0ifCF==0,~0ifCF==1

noteax;

~0ifTOSdidnotwrappedaround,0otherwise

andecx,eax;

setto0ifwraparound

moveax,esp;

currentTOS

andeax,not(_PAGESIZE_-1);

Rounddowntocurrentpageboundary

cs10:

cmpecx,eax;

IsnewTOS

jbshortcs20;

inprobedpage?

moveax,ecx;

yes.

popecx

xchgesp,eax;

updateesp

moveax,dwordptr[eax];

getreturnaddress

movdwordptr[esp],eax;

andputitatnewTOS

ret

Findnextlowerpageandprobe

cs20:

subeax,_PAGESIZE_;

decreasebyPAGESIZE

testdwordptr[eax],eax;

probepage.

jmpshortcs10

_chkstkendp

此函数较之前的要稍微复杂一些,不过代码还是非常清晰易懂的。

还是解释一下吧,先来看leaecx,[esp]+8-4这句,与_alloca_probe_16汇编代码相比较,多了一个减4,这里减4是因为从_alloca_probe_16函数到_chkstk函数之间是用的jmp,而不是call,因此没有返回地址,只有保存的ecx值的4个字节,所以少4个字节的偏移就能取到esp的值了。

由于_alloca_probe_16函数是保持栈平衡的,并且没有改变esp的值,因此,_chkstk函数里取到的esp与_alloca_probe_16函数取到的esp是一样的。

并且也都存放到了ecx中。

后面一句与_alloca_probe_16函数的逻辑一样,都是将ecx(esp的值)减去eax(要分配的栈内存大小,已经由_alloca_probe_16函数对齐过)。

这一句之后,ecx的值就是新的esp的值,如果栈没有溢出,那么esp将会被设置为这个新值,于是栈内存分配成功。

继续向下分析,紧接着下面3句,用得有一点巧妙。

sbbeax,eax,sbb乃带借位减法指令,如果前面的subecx,eax存在借位(ecx小于eax),则sbb之后eax的值为0xffffffff,然后再noteax,eax将变成0,然后再andecx,eax,则ecx变为0,也就意味着新的esp值为0。

这里先放一下,待会儿再向下分析。

再看前面,subecx,eax存在借位,为什么会存在这样的情况,难道_alloca_probe_16函数不检查申请内存的大小的吗?

的确,他并不会关心你想申请多少字节,他只是与_chkstk配合,让_chkstk能够知道申请的内存过大就可以了,过大之后可以由_chkstk进行检查并抛出异常。

那么我们来看_alloca_probe_16函数是怎么配合_chkstk函数的检查的呢。

这又得回到_alloca_probe_16

函数的汇编源代码中,看这三句:

addeax,ecx;

sbbecx,ecx;

oreax,ecx;

eax为申请的大小,ecx为新的esp值,由subecx,eax计算获得。

把这三句代码与_chkstk函数的三句代码结合着看,这里如果eax过大(申请空间过大),addeax,ecx之后,会溢出,即CF位为1。

然后执行下一句sbbecx,ecx,也就等同于:

ecx=ecx-ecx-CF=0-1=-1=0xffffffff。

然后在oreax,ecx,于是eax为0xffffffff,也就是传给_chkstk函数的申请空间大小。

然后再看前面对_chkstk函数的分析,如果eax为0xffffffff,那么肯定会sub溢出,于是ecx(新的esp值)最后为0。

再看另外一种情况,如果在_alloca_probe_16中,eax的值大于ecx的值,那么sub之后,会溢出,在andecx,(16-1)之后,再addeax,ecx,此刻假设不会溢出,sbb之后,ecx为0,之后再oreax,ecx不会影响eax的值,但是此时eax还是大于ecx(esp的值)的。

当eax传入_chkstk之后,sub会溢出。

与eax为0xffffffff的结果一样,都使得ecx(esp的值)的值为0。

所以由上面两种情况分析下来,_alloca_probe_16函数和_chkstk函数之间是有一定的配合的。

也可以说是_alloca_probe_16函数适应了_chkstk的检查方案。

我们再继续向下分析_chkstk吧,看后面两句,先是moveax,esp将当前的esp值交给eax,注意这里的esp值是_chkstk内部已经压入保存了ecx原始值之后的esp,这个esp也就是最初有leaecx,[esp]+8-4获得的上层esp值减4(pushecx占用的4字节)。

获得了当前esp值之后,又andeax,not(_PAGESIZE_-1),_PAGESIZE_为0x1000,也就是4096字节(4KB),即为windows页内存大小规则之一。

这句代码也就是将当前esp所在的页剩下的字节全部减掉,到达这一页的末尾下一页的开始。

这样做是方便后面的栈溢出检查。

之后,有两个标签cs10和cs20,cs10的开头是判断ecx是否小于eax,此刻的eax已经是某页的开头,如果ecx小于这个eax所存的地址值,则跳转到cs20标签里,cs20标签里代码很简单,进入就将eax减掉一页内存,然后是testdwordptr[eax],eax这句,这句存在一个内存访问,可以想象如果eax所存的内存值不可读,那么就会抛出异常。

这里正是利用这一点,当这里不异常,又会跳转到cs10标签里继续比较,如果还是小,则在减一页,再进行访问,直到ecx大于等于eax或者抛出异常。

那么再想一下上面分析的逻辑,如果申请的空间过大,ecx的值会为0,那么在cs20中判断,0会一直小于eax,这样eax会一直减4K,直到eax为0,这里显然减不到0就已经抛异常了。

当eax减到一定时候,则会在testdwordptr[eax],eax这句抛出一个栈溢出的异常,如下图:

如果继续执行,则会发生访问异常。

如果申请的大小不会导致栈溢出,则当eax减到一定时候ecx大于等于eax,或者第一次进去时ecx就是大于等于eax的,则进入正常开辟空间的逻辑:

moveax,ecx;

popecx

xchgesp,eax;

moveax,dwordptr[eax];

movdwordptr[esp],eax;

ret

第一行是将ecx(新的通过验证的esp)赋值给eax,然后是还原ecx的值,第三行就是将当前的esp值和eax做交换。

esp便是开辟空间后的新值,此刻肯定比eax的值要小(栈向低地址延伸)。

然后是第4句,此时eax是popecx之后的esp值,也就是call_alloca_probe_16函数压入了返回地址后的esp值,因此,第四句执行后,eax的值就是,_alloca_probe_16函数函数的返回地址,我们准备返回到上层,这里的上层不是_alloca_probe_16函数,因为他们之间不是call的,而是jmp的,不存在返回地址压入。

这里的上层是_alloca_probe_16函数的上层。

第5行,是将eax存入当前的esp指向的内存中,因为下一条指令ret,即将读取这个地址,并返回到上层,其间的原理请参考《InlineHook之(监视任意函数)》,此文有相同的用法。

整个过程就是这样了,其实在很多C语言编写的实际项目中,还是有用到alloca。

就我个人而言,我觉得不管他有什么优点和缺点,只要弄清楚了他的这些特性,完全可以规避他的缺点,而发挥他的优势。

而且也确实动态分配适量的栈空间,能获得一些性能。

本文只是为了介绍其原理和细节,不在此争论辩证性的论题。

如果要使用alloca,可以非常简单的使用,如下:

voidfunc(void)

{

int*p=(int*)alloca(4);

*p=100;

}

不用自己管理释放,当函数结束时,esp会平衡。

另外,需要提到的是,根据alloca申请的大小的变化,编译器可能在后台做一些调整,比如当申请的内存较小时,alloca直接被编译成_chkstk,而不会调用_alloca_probe_16函数,这也算是一个小小的优化吧。

再比如,在VS2003下,不管申请多大的空间,都会将alloca直接编译成_chkstk。

因为vs2003的CRT没有提供_alloca_probe_16函数的实现。

上面提到的alloca,在VC的CRT中其实是一个宏定义,#definealloca_alloca。

另外还有一些CRT宏定义,例如_malloca,这个宏定义也等于是一层封装,在debug下,_malloca调用的是malloc,在release下,当申请的大小小于一定值时,调用的是alloca,否则调用malloc。

因此,需要调用_freea来释放内存,_freea会根据标记,判断是malloc分配的还是alloca分配的,如果是malloc分配的堆内存则调用free,如果是alloca分配的栈内存,则不用释放。

//_malloca的定义

#ifdefined(_DEBUG)

#if!

defined(_CRTDBG_MAP_ALLOC)

#undef_malloca

#define_malloca(size)\

__pragma(warning(suppress:

6255))\

_MarkAllocaS(malloc((size)+_ALLOCA_S_MARKER_SIZE),_ALLOCA_S_HEAP_MARKER)

#endif

#else

((((size)+_ALLOCA_S_MARKER_SIZE)&

=_ALLOCA_S_THRESHOLD)?

\

_MarkAllocaS(_alloca((size)+_ALLOCA_S_MARKER_SIZE),_ALLOCA_S_STACK_MARKER):

_MarkAllocaS(malloc((size)+_ALLOCA_S_MARKER_SIZE),_ALLOCA_S_HEAP_MARKER))

//_freea的定义

_CRTNOALIAS__inlinevoid__CRTDECL_freea(_Inout_opt_void*_Memory)

unsignedint_Marker;

if(_Memory)

{

_Memory=(char*)_Memory-_ALLOCA_S_MARKER_SIZE;

_Marker=*(unsignedint*)_Memory;

if(_Marker==_ALLOCA_S_HEAP_MARKER)//判断是否是堆标记

free(_Memory);

}

#ifdefined(_ASSERTE)

elseif(_Marker!

=_ALLOCA_S_STACK_MARKER)

_ASSERTE(("

Corruptedpointerpassedto_freea"

0));

//_MarkAllocaS的定义

__inlinevoid*_MarkAllocaS(_Out_opt___crt_typefix(unsignedint*)void*_Ptr,unsignedint_Marker)

if(_Ptr)

*((unsignedint*)_Ptr)=_Marker;

//打上标记,_ALLOCA_S_STACK_MARKER或_ALLOCA_S_HEAP_MARKER

_Ptr=(char*)_Ptr+_ALLOCA_S_MARKER_SIZE;

return_Ptr;

【延伸】

这里延伸一个玩儿的用法,就是在写C语言程序时,有多个函数参数是指针并且参数个数一样,这些函数的指针参数的类型都不一样,在C++里有template,在C里可没有。

于是为了实现一个类似功能的东西,我们就可以用alloca来申请参数的空间,然后调用函数。

#include&

stdio.h&

malloc.h&

voidfunc(char*p)

printf("

%s\n"

p);

voidchk(void*arg)

if((void**)arg-&

amp;

arg!

=1)//检查参数的位置是否紧挨着arg所在的内存地址

__asmint3//如果紧挨着,当chk执行完之后,esp即刚好指

}//alloca申请的空间,因此,

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 经管营销 > 财务管理

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1