Dll的编写(上)

dll是非常实用代码复用机制,所有的方法在物理内存或者disk page中只有一份,数据可以是一份也可以定义为多份。这就为数据,方法共享和多任务操作,提供了非常大的便利。

——————>dll

dll当然是PE格式的文件,就像许多其他含有公用方法的文件一样,这些功能上都算是动态链接库。

当然了,关于PE文件,还得另行研究了。

——————>编写要点

1 dll同样可以使用资源,同样可以导入其他动态链接库的函数。

2 dll的方法被某PE使用之后,这个方法就相当于是该PE自身的,没有区别。

3 dll需要一个入口方法,也就是MSDN中的DLLMAIN

DllEntry proc _hInstance,_dwReason,_dwReserved
mov eax,TRUE
ret
DllEntry Endp

这里得到的_hInstance是dll本身的handle。也是唯一一次dll得到自己的handle 的机会

_dwReason有4种:

DLL_PROCESS_ATTACH

得到这个消息就表示,dll已经载入到线性地址中了。

这里可以做一些申请保留地址,以及初始化工作。

DLL_PROCESS_DETACH

可以做释放内存的工作,或将打开文件关闭等等工作。

DLL_THREAD_ATTACH

得到这个消息的时候,说明这个进程新建了一个线程。

MSDN上说这个时候可以去做一个TLS slot

DLL_THREAD_DETACH

这里可以把做TLS slot的memory 释放。

——————>也是,关于内存的操作一般都写在dll中的,exe都是处理逻辑上的事物,方法和模块都另外封装吧。

4文件的最后结尾是:end  入口方法名

5写完之后,我们还需要写一个def文件,这个是链接器需要的

要是详细了解还是要参看MSDN的”链接”这一个关键词

一个主要的写法就是:

EXPORTS

方法名A

方法名B

也可以定义变量。

(因为类里面有公有和私有,而EXPORTS的意思就是公有化。)

——————>link的选项

基本写法就是

/subsystem:windows /Dll  /DEF DEF文件名 obj文件 RES文件

——————>

这里面,如果需要数据段共享,那么可以加上个选项/Section .bss,S

MSDN上对这个选项的解释不太明确,还是要参考一篇文章:

http://msdn.microsoft.com/en-us/library/ms809762.aspx

——————>

编译链接之后,会生成需要的dll和lib文件

编程使用的话,需要lib来找dll

要是使用include的方式来加载lib,则需要用写一个prototype列表文件。

——————>

prototype在JavaScript里面有很广泛的应用,但是之前我在C里面是根本没有见过这个词

不过现在MASM里面却是出现了这个伪指令PROTO,其实这个词的意思就是函数声明,是编译器需要的。

感觉一部分用途是生成方法入口地址,另一部分是提供一个函数的参考模板,看看参数的数量类型(这个汇编里面没有返回值数量类型一说。。)是否匹配

prototypelist也就是模板的主要内容了。

所以,prototype就是个set default structure的意思。

——————>

回到dll的使用中

这里有2个扩展应用,一个是动态加载dll。另一个是其他语言加载汇编的dll

————————————>动态加载dll

这个比较麻烦,不过却解决了很多问题。反正大概知道有好处就是了,反正好处越大,代码就越复杂。。

就像传统的C和高级语言相比,写起来慢,但是好处是巨大的,这个不用说的。

动态加载dll涉及到3个API方法:

invoke LoadLibrary,addr szDll

参数是Dll的名字的string,如果成功,返回值是dll的handle(每个进程得到的handle是不一样的)

invoke GetProcAddress,hDllInstance,addr szIncCounter

第二个参数是存有公开的方法名的,string。返回值是方法的入口地址

invoke FreeLibrary,hDllInstance

释放这个dll。

——————>

1。操作系统会给dll上加个计数器,每加载一次+1,每free 1次就-1。当为0时,清楚掉dll在内存中的数据

2。要调用的时候,需要把dll的名称和方法都存到const里面去,这样也是麻烦不小。

3。调用过程,这个方法我们不能使用invoke。应该是编译器认为这只是个地址而不是的proc的缘故。毕竟我们之前没有函数声明。

所以我们变通的写两个数据类型的宏,即用typedef

先做个proto数据类型的宏,然后给这个宏的地址再用typedef做个宏。

_PROCVAR4 typedef proto :dword,:dword,:dword,:dword

_PROCVAR3 typedef proto :dword,:dword,:dword

_PROCVAR2 typedef proto :dword,:dword

_PROCVAR1 typedef proto :dword

_PROCVAR0 typedef proto

PROCVAR4 typedef ptr _PROCVAR4

PROCVAR3 typedef ptr _PROCVAR3

PROCVAR2  typedef ptr _PROCVAR2

PROCVAR1 typedef ptr _PROCVAR1

PROCVAR0 typedef ptr _PROCVAR0

我们就可以定义PROCVARx类型的指针数据了。

这个可以用invoke 来传参数了。

——————>

要让这个lib在C以及C++可以调用,那么,就必须写个头文件了。

要更深一步,比如在C和C++里面动态调用dll。我就不知道了。

头文件大致这样写:

#ifdef __cplusplus

extern “C” {

#endif

__stdcall _IncCounter();

__stdcall _DecCounter();

__stdcall _Mod(unsigned num1,unsigned num2);

#ifdef __cplusplus

}

#endif

在C和C++里面,汇编的返回值,那就是eax中,以unsigned表示了。

不甘心呢。

——————>

心情很糟糕,非常糟糕。

学习之事需要加速了。

自己非常不现实,可是急功近利并不能达到我的目标。

可是,又可是,不急功近利,如何独立生活?

只有足够的复杂才能引起我的兴趣,而要战胜这些复杂的问题,需要的是花费我这不太聪明的人许多的时间。

可就是如此的矛盾,如此的跟不上别人脚步的我,却又不甘于泯灭于红尘。

混账呢。。自己如此的不长进,如此的不开窍。

——————>

我总是一个人,自己是自己的老师,自己是自己的徒弟,自己是自己的主人,自己又是自己的奴隶。

自己给自己自由,自己关自己笼子。

——————>

可是不甘呢。我活着的意义不要是平庸。我要在某个领域里,做到最好。

我真的没办法做到一直一直集中在某一件事上。平庸呢

我只能把干扰我的事情,一件一件,慢慢减少。但是干扰我的事情在某段时间,会多起来,我还得一件一件的去剔除。耗费的还是时间,痛心呢。

——————>

为什么我没有那些天赋?

为什么我有些东西明白得那么晚?

为什么总是觉得有2个自己,当一个自己没有集中的时候,另一个颓废的自己就乘虚而入?

忘却了过去的种种,战胜了未来的诱惑,可是却敌不过现在的迷茫。

——————>

愧疚!!!一文不值又无可救药的东西!!

爱自己?对,我恨极了自己!!!

windows内存管理(3)

虚拟内存管理函数

这个管理函数用处是什么?

——————>

一般情况下,我们申请内存,拿来用就可以了。

但是考虑到多线程的关系,内存空间很难连续的扩展,想要扩展就得令开新地。所以想要一块可以随时扩展的内存空间,就需要某种预留机制。

还有一些对已经使用的内存空间的保护限制机制,也是在这里完成的。

——————>

我们保留的一大块地址中,可以分小块的提交。当然,提交之后被分配到物理内存还是Page中,我们不知道。

保留的方式是:

invoke VirtualAlloc,Null,SIZE,MEM_RESERVE,PAGE_NOACCESS

我们让系统给我们一个size大小的地址空间,这个返回的是入口地址lpMemory。

这一段地址空间的内部是自动置0的

——————>这个SIZE要是4KB的整数倍,因为提交和释放都是按页为单位的。

提交的方式是:

invoke VirtualAlloc,lpAddress,SIZE,MEM_COMMIT,PAGE_READWRITE

这里的最后一个参数,表示提交之后page的属性,不能是NOACCESS了,其他都可以。

我们要Free释放这一块保留地址,那就要把这里面你的全部改为提交或者不提交。

也就是我们要调用

invoke VirtualFree,lpMemory,SIZE,MEM_DECOMMIT

然后再调用

invoke VirtualFree,lpMemory,0,MEM_RELEASE

这样就是一个 保留-提交-释放  的过程。

——————>

invoke VirtualAlloc,NULL,SIZE,MEM_RESERVE or MEM_COMMIT,PAGE_READWRITE

这个方法与GlobalAlloc没有什么差别,唯一的好处是,这个里面的NULL可以置为我们想设定的地址。

——————>

使用的过程中,其实还可以保护数据不被改写。

即在写入数据之后,将这一段pages的属性改了:

invoke VirtualProtect,lpAddress,SIZE,flNewProtect,lpfOldProtect

flNewProtec这个参数可以设定为PAGE_READONLY来保护数据。

lpfOldProtect需要指向一个dw变量,存储的是之前,第一个页面的属性。

(MSDN上说是必须指向正确的属性,如果是NULL则Function fails)

——————>

http://msdn.microsoft.com/en-us/library/ms810627.aspx

这里一篇古老的文章,93年写的。不过也是很好的参考了。

这里面有一个VirtualProtect的应用的想法。

背景是这样的,有一段内存地址要做缓冲区,当然这个缓冲队列的大小有限制。当某线程A不断往里面输入数据,另一个线程B不断的读取数据。如果某一段时间,由于线程B的读取没有拿到时间片,那么缓冲区就有可能会满掉,这时候会有溢出。

解决方法就是在队列的顶端page设为NOACCESS。这样当要用到这最后一个page,就会发出expction消息。那么,就可以用某函数处理这个消息。那么,改page为READWRITE,挂起线程A,改为线程C来慢速地注入数据。并当队列中的栈顶到某一合适值时,回到A中来接受数据。

——————>

想要把所有的已提交的memory pages放到physical memory中,那就要用VirtualLock了。

我想,这个时候的操作应该是在内存中申请一块区域,然后page中的数据复制到内存中。

page数量的多少由SetProcessWorkingSetSize来设定。

解除锁定就是用VirtualUnlock。或者进程终止会自动解除。

——————>

移动内容和填充内容都可以用汇编来完成,win32的API很慢的。

——————>

测试page的可读性可写的属性,可以去找IsBadCodePtr之类的方法。

windows内存管理(2)

详细方法在MSDN里面找:

Memory Management Functions

——————>Global and Local Functions

上一篇就是这个标准内存管理函数,其实明确一点说,就是Global and Local Functions。

它是Memory Management Functions是一个最低级的分支了,因为它主要是为了兼容16位指令。

所以它比其他的管理函数要慢上许多。并且他分配的内存是进程私有的,无法共享给其他进程。当然了,进程自己的线程还是共享的。

据说只有Dynamic data exchange和clipboard functions还在用着Global and Local Functions。16位时代的技术,都淘汰了。

——————>Heap Functions

这个应该就是32位最常用的技术了。

私有堆和默认堆

程序初始化时由操作系统给的内存空间就叫做默认堆,Global and Local Functions就只能在这默认堆中进行操作了。

两者要进行一下逻辑上的比较,不过在这之前要知道什么是线程

——————>线程

进程可以创建一个或者多个线程。逻辑上就是这么说了,也就是可以把一个进程中的指令分组,不再是顺序执行,对于CPU来说,肯定是利用时间片轮转。接下来稍微说说时间片轮转

——————>时间片轮转

其实对于每个进程来说,都有一个主线程,也就是单线程的进程,只是这个我们感觉不到而已。当然,CPU是知道的,时间片轮转的概念就是对应着线程。

也就是操作系统给每个线程一个最大执行时间并命令CPU去执行,如果这个时间还没执行完,就去执行其他的线程。

这个具体的时间我不太清楚,不过我的感觉也许是10W~1亿个clock吧。

当然了,每个在轮转的结束,要存储一下寄存器的值和内存入口地址,这个时间不会长,就几十个clock而已。

这个时间片轮转和线程都是操作系统的里面的概念,对于CPU来说,它并不知道,它只知道执行

——————>这里面出现了:clock

这个是CPU的执行指定对应的时钟,按我的理解,每20ms是2GHZ来说,那就是10亿次执行route。

但这个route不是最小的单位,更小的是指令或者说是运算。

每个执行route的时间就是clock,具体就是10亿分之一秒吧。

每个route中可能包含多个运算,具体就不知道了。

奔腾初期的时候,一个route就是一条指令,比如mov eax,ebx。现在都酷睿多核了,谁知道呢?

以上都是逻辑上的推理而已。

——————>为啥叫Thread?

回到线程,进程既然可以有多个线程,里面的指令都可以被交叉执行,所谓我们知道了为啥要把这种交叉执行的机制叫做是Thread。因为线来织布,就是交叉交叉的。这又是操作系统给我们的一个比喻。也是很生动的说明了,这就是我们整个计算机软件的载体。我们就是在织梦而已。

就像对于CPU来说,它的操作器就是operand,更明确一点就是register segment。我们可以理解成,CPU整个运算中心的登记处。。不如thread来得可爱哦。

至于opcode(binary,hex),string(ASCII)之流,不过就是数据的材质而已。

嘿嘿。动画上的词都给我用上了。

——————>回到堆

1.默认堆只有一个,私有堆有多个。这也就是说,这里可以让不同线程管理不同的private heap

2.私有堆的再刚开始分配时,有个扫描内存链的过程,这个我不懂,不过想想也知道私有的东西,都应该有个排序和其他的flag

3.私有堆的好处在于减少physic memory 和disk page文件交换,因为他把访问的内存局限在一个小范围,访问disc page的可能性大大降低了。

(disk page就是平常意义上的虚拟内存,其实把他称为交换页比较好。其实对于我们程序员来说,因为所面对的都是线性地址,这个线性地址映射到了物理内存和disc page。我们一般也把线性地址成为虚拟内存。)

4.有利于封装和保护模块化程序。因为当越界情况出现。private heap可以很容易跟踪定位。

5.有利于内存清理,默认堆的内存清理要一个一个来,而私有堆的内存清理就把堆释放就可以了。

——————>

heap里面的方法足以替代Global方法了。所以用这个就够了。

——————>序列化的问题

这里有个独占性检测的问题。所谓独占就是一个线程控制一个heap

那么,与之相反的呢,就是多个线程控制一个heap。那么,如果是这种情况,就需要排队了。

因为处理步骤是多步的,只有一个线程处理完了,另一个才能处理,否则就乱了套了。

操作系统为了解决这个问题会进行一个serialization的操作,确定是否这时候就1个线程来独占heap。

当然了,如果每次都这样做,系统消耗是明显的,所以我们就不要这个操作了,自己把heap单独分给线程就是了。

这种方法就是提高了速度,但是增大了内存消耗,不过这也是应该的。

或者同样的Lock再UnLock,但这个锁定操作很少使用。

——————>

创建堆的时候, 指定了HEAP_NO_SERIALIZE的话,其他操作函数就不需要再定义这个flag了。

其他操作Alloc,ReAlloc,Size,Free都会在内部调用Lock和Unlock。

用于检测Heap的函数有3个:

GetProcessHeaps;得到所有的堆,存到一个缓存中。

HeapWalk;得到列出堆中所有内存块,貌似这个不会内部调用Lock

HeapValidate;检测内存块或者堆的完整性

还剩下2个:

HeapCompat;合并堆中空闲内存块,释放不在使用中的内存page

HeapSize;返回内存块大小。

——————>

其实Heap其实省略了一个步骤,就是线性地址与真正的物理地址或者disk page进行着怎样的操作。

这一部分Heap是负责不了的。Heap只是对线性地址的一个分配管理工作,是对固定内存分配管理的基础上的扩充

也就是Global and Local Functions 的速度和线程优化方法。

windows内存管理(1)

——————>

堆??

堆栈,其实都是一样的,因为对于CPU而言,它只有栈寄存器,也只有栈的汇编指令。

堆这个东西,不过是我们用一些代码实现的功能而已。

在一个C写成程序,运行之后,会有一段调用int21h ah=4A的中断来申请内存。

然后呢,就给这段内存取了个名字,叫做堆。。

——————>为什么叫堆呢?

我估计的原因:

因为C存储变量的时候,利用BP来存,它是把先定义的变量存的地址总是最小的。

取数据的时候也是用寄存器来取,地址都是编译时候就决定好了的。

我们把高位的地址叫做栈顶,这个也可以说是堆的顶吧。那么堆的底在哪里呢?也就是第一个变量的入口地址。。

其实都是这些都是逻辑概念而已,对于内存来说,这一切都是浮云。

——————>

现在碰到过的2个堆,一个是键盘缓冲区,一个就是FPU了。

这两种都是硬件指令上实现的堆。而内存只有硬件上的栈,逻辑上(自己造)的堆。

所以,其实堆这个概念应该是从C语言之后才开始有的,又因为Windows是C语言写的,所以,Windows自然,必须,绝对要有个堆管理器。。。

为什么C语言不喜欢栈呢。。也许是因为变量的存储太复杂了,不得不顺序的来取数据。、

——————>

烦恼。。写个64位2进制数转成ASCII码都转掉了我3个小时。。晕。。

显示内存情况

——————>开始基础的内存管理(头有点晕,这个破代码害的)

1分配固定内存

invoke GlobalAlloc,GPTR,1000000

参数1是固定内存和全部置0的意思

参数2是字节大小

如果成功就返回一个lp指向内存入口地址。如果不成功就是Null

释放内存:

GlobalFree

调整大小:

invoke GlobalReAlloc,lpMemory,size,GMEM_ZEROINIT or GMEM_MOVEABLE

移动内存之后入口地址会变。缩小时候不会变。如果要扩大,必须用这种得到新入口地址的方法。

——————>

2可移动内存

invoke GlobalAlloc,GHND

返回的是一个内存的handle而不是lp了。并且还有65536的个数限制。

移动之前要Lock它才行。

GlobalLock,handle

返回值是lpMemory,这个就像固定内存一样了。

解锁:

GlobalUnlock。返回的是handle

——————>系统会维护一个锁定计数,就是锁定1次需要解锁一次,锁定2次需要解锁2次

释放通过的时候,同样是GlobalFree,无论锁定与否

调整大小时,返回的还是原来的handle,但是之前GlobalLock得到的loMemory就可能没用了。

所以还是要GlobalUnlock一下,再移动,再GlobalLock

——————>

固定内存块的使用限制于速度要求快。内存使用不太频繁,或者虽然频繁但内存块大小一致的情况。

但不适合长时间运行程序。

——————>

3可被系统抛弃的内存块

invoke GlobalAlloc,GHND  or GMEM_DISCARDABLE,size

当GlobalLock锁定内存的时候,如果返回Null,就说明被抛弃了。但是handle还是有用的,可以用GlobalReAlloc来重新分配内存。

当内存卡的锁定计数为0时,也可以用GlobalDiscard来抛弃指定handle的内存。

————————————>

NULL指针检测,禁止越界

——————>

4获取内存块的信息

GlobalFlags

主要是来获取可移动内存块的lock和是否被丢弃的。

调用之后的返回值如果不是GMEM_INVALID_HANDLE则表示成功

低位的数据用and GMEM_LOCKCOUNT可以得到。表示的是锁定计数器的值

高位的话,可以AND FF00得到,如果和GMEM_DISCARDED相同,就表示已经被丢弃。

GlobalHandle

可以从GlobalLock的lpMemory得到handle

GlobalSize

得到一个内存块的尺寸。

第 20 页,共 31 页« 最新...10181920212230...最旧 »