驱动知识积累-----汇编,C语言之变量,函数透析

驱动知识积累-----汇编,C语言之变量,函数透析

(2010-08-15 17:40:18)
标签:

offset

addr

&

函数

变量

 

汇编,C语言之变量,函数透析

关于变量,首先要明白他有三个概念:变量名,变量值,还有变量占据的地址空间。

关于初学者,特别是同时学过汇编和C语言的读者肯定会对变量产生迷茫感。正因为如此,很多读者会问这样的一个问题:什么时候用offset,为什么同样是对变量的使用,有些就加offset,有些加不加效果一样,而有些必须得加。还有朋友会问:offset究竟是什么东西?当然,你可以百度搜索下,很多人会告诉你其作用是取偏移地址。不过,嗯嗯嗯,其实这样的说法有些误导的感觉,或者说不深入,至少这样的答案不能满足我的要求,很多东西还是无法去解释通。而现在,我来告诉大家这些东西究竟如何去解释。

不知道大家注意到没有,当你调用API函数的时候(不管是内核,win32,还是native),这些API里面所有的参数都是dword类型。继续,我们调用API函数所传递的参数无非就是两种:1,数值 2,字符串。(当然也会涉及到结构)

好,如果是传递数值,那么直接把数值push进堆栈就OK,而如果传递字符串的话,那就必须push字符串的首地址才行。毕竟4字节(dword)空间怎么都放不下一个字符串。这样的话,当API函数执行的时候,遇到数值,那么直接拿来用,如果是字符串,那么通过压栈的地址值找到想要的字符串。

以上问题明白了,你必须还要明白另一个概念。对于变量而言,他其实只是某段地址空间的代号,你在编写程序的时候经常会用到变量名,但在编译的过程中,编译程序会将这些变量名直接替换成对应内存的首地址。举例:以下代码源自《琢石成器》第13章节(P496)有书的朋友可以直接翻到这页,没书的请看下面代码:

_lpLoadLibrary dd ? ;导入函数地址表 (1)
_lpGetProcAddress dd ? (2)
_lpGetModuleHandle dd ? (1) (3)

........

........

_szClassName db 'RemoteClass',0 (4)
_szCaptionMain db 'RemoteWindow',0 (5)
_szDllUser db 'User32.dll',0 (2) (6)
_szDestroyWindow db 'DestroyWindow',0
.......

.......

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_RemoteThread proc uses ebx edi esi lParam
local @hModule

call @F
@@:
pop ebx
sub ebx,offset @B
;********************************************************************
_invoke [ebx + _lpGetModuleHandle],NULL (3)
mov [ebx + _hInstance],eax
lea eax,[ebx + offset _szDllUser] (4)
_invoke [ebx + _lpGetModuleHandle],eax
mov @hModule,eax
lea esi,[ebx + offset _szDestroyWindow]
lea edi,[ebx + offset _lpDestroyWindow]
.while TRUE
_invoke [ebx + _lpGetProcAddress],@hModule,esi
.......

.......

我们开始从头研究:(这个时候你需要点PE方面的知识,我就当你有这方面的基础了)

1,是这样的,编译器在编译这段代码的时候会智能的做很多事情,当他分析(1)这句话的时候,编译器就会认真记录这个变量的变量名和将来映射到地址空间的首地址,之后再分析(2)这句话,他同样会认真记录此变量的变量名和对应的地址空间的首地址。研究表明,这2个变量的地址是连续的,当然你暂时并不需要重视这个特性。接下来就到了(1)这句话,编译器同样还会认真记录下变量名和地址空间的首地址。

2,上面说了,编译器会分析你写的汇编代码,并且都认真记录下相关的信息。那么为什么要如此认真的记录呢。哦原来是这样的,当编译器分析到(3)的时候,一旦发现代码里出现变量名,编译器二话不说直接替换成其对应的首地址。也就是说,假如_lpGetModuleHandle的首地址是00500000,那么(3)这句话可以翻译成

_invoke [ebx + 00500000],NULL

同理,假如_szDllUser的首地址为00501000,那么(4)这句话可以翻译成

lea eax,[ebx + 00501000]

以上这个替换是编译器自动替换的。继续,我刚才说了很多人对offset理解并不是很透彻,但是至少说此伪指令的确有“取地址”这个意思在里面,暂且你可以先这样理解,这样一来,上面一切都可以说的通。但是有读者会问了:既然都是变量的使用,为什么(3)就不加offset,而(4)就加offset呢?还有,为什么加offset和不加offset效果都一样?为什么编译器对这两句话都用同一个方式对待?

我来回答这个问题:首先,编译器的确没有做错,并且就上面2句话而言,加不加offset没有任何区别。如果有读者不信,你完全可以在上面2句代码上都加上,或者都去除offset,或者一个加一个不加,或者调换,程序都能正确运行。就目前而言,我只要让你知道一件事情----编译器一旦发现变量名,那么立刻替换成地址值就行,接下来我再举个例子,再比较一下,那么你就知道offset的真正含义了。

我们来看一下CreateProcessA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD

这是在win32汇编中创建进程函数的定义。由于参数比较多,我就暂且认为它只包含2个参数(因为这里只是举例,所以就暂且提出这样一个错误的假设,还请读者理解),一个是数值,一个是字符串。这样这个函数的定义就是:CreateProcessA PROTO :DWORD,:DWORD。并且我还告诉你,这个函数的第一个参数是数值,其意思就是告诉此函数创建几个进程,第二个参数是字符串,告诉函数第一进程的进程名。这样我写出这样几句代码:

.data

_count dd ? ;假如_count的地址为00501000

_name db "my name is UFO",0 ;假如_name的地址为00502000

start:

.code

invoke CreateProcessA , _count ,offset _name ;假如createprocessA的地址是00503000

end start

需要注意的是,编译器会把 invoke CreateProcessA , _count ,offset _name 变换成

push 00502000 ;第二个参数先入栈

push [00501000] ;第一个参数后入栈

call 00503000

这样你看出差别了吧,正如一开头我所说,任何API函数接收的参数要么是数值,要么就是字符串。而编译器有个特性:当他一旦看到代码里出现变量名,那么它头脑里第一个念头就是先把变量名替换成具体的地址值,之后再在这个地址值外围加上一个[]。除非你特意告诉编译器,我必须传给API函数的参数是个地址值。从中你可以发现,offset伪指令的作用就是刻意告诉编译器,你别给我自作主张的加上[]号了,我不需要你如此的主动

这样一来,当CPU执行上面3行指令的时候,先是把字符串的首地址压栈,之后再到内存00501000这个地址中取出具体的数值后压栈。最后调用函数。哦,原来offset的作用是这样的。哈哈

我们再来看看addr这个伪指令。请读者翻到P74页,书上讲的东西还是比较好理解的。我再补充总结下:

offset是针对全局变量而言。addr是针对局部变量而言。为了让读者有理性认识,我们还是举例:还是拿上面的CreateProcessA 函数为例。

当某个函数调用此API函数的时候 其堆栈情况是这样的

push 第二个参数

push 第一个参数

push 返回地址

push 调用函数的ebp (函数调用框架,此调用框架属于调用者)

mov ebp,esp (成立CreateprocessA函数的调用框架)----这一步不涉及到堆栈,写出来是方便读者阅读

push CreateprocessA函数内部定义的第一个局部变量

push CreateprocessA函数内部定义的第一个局部变量

.......

.......有多少局部变量就push多少。具体由具体函数决定

上面蓝色的ebp为调用函数的框架,红色的ebp是被调用函数的框架。如果读者对函数调用框架不了解,那么请参考天数夜读这本书。

上面的代码是编译器根据“invoke CreateProcessA 第一个参数,第二个参数” 这条指令翻译成的。由此可以看出,第一个参数为 ebp+8 第二个参数为ebp+12 第一个局部变量为ebp-4 第二个局部变量为ebp-8 这样,编译器一旦看到局部变量的变量名,他就会立刻把变量名改写成诸如ebp + X 这样的形式,同时再加上[]。哎,和全部变量类似,这个举动是自动的。而这个时候的addr就和offset的作用相同,此伪指令的意义在于特意告诉编译器,别那么积极,我不需要你帮我加上[]这个符号,我讨厌你的帮忙。

好了,到现在为止,你应该理解了offset 和addr这两个伪指令。 继续

你回过头来看我举的第一个例子:

_invoke [ebx + _lpGetModuleHandle],NULL (3)

你会发现,如果按照上面讲的理论来理解这段代码,就会出现问题。因为如果按照上面的理解方式,这段代码必须写成 _invoke [ebx + offset _lpGetModuleHandle],NULL才能说的通。关于这个问题,还请读者参考下编译原理相关的书,关于为什么可以不加offset,笔者不是很有把握。不过呢~还请读者放心的是,以上结论都是正确的。当然,不管你能不能找到这方面的书来理解这个问题。为了自己代码的可读性,这个offset不用省就是了。

虽然我并不是很有把握,但是我有个猜想(应该是正确的):一旦编译器发现一个变量名和一个数值相加或者相减,那么此时此刻编译器就不会主动加上[]号,仅仅只会把变量名替换成地址值。这样一来,你就必须手动加上[]号了。正如你在代码中所看到的那样!

总结:

以上说了半天,你会发现,所谓的offset或者addr,他们两个家伙压根没有取地址的功能。所谓的取地址这个举动是自然而然的举动,并不是offset来做的。正如你所看到的,编译器在分析代码的时候一旦发现变量名就立刻替换成变量的地址,并且同时会在外围加上[]号。这涉及到2个步骤的操作。而一旦在变量名前加上offset就相当于告诉编译器省去第二个步骤的操作,仅仅完成第一步操作就行了。

变量研究好了,我们现在开始研究win32汇编里的函数。请读者把书翻到P501页。没有书的读者请看下面:

invoke VirtualAllocEx,hProcess,NULL,REMOTE_CODE_LENGTH,MEM_COMMIT,PAGE_EXECUTE_READWRITE
.if eax
mov lpRemoteCode,eax
invoke WriteProcessMemory,hProcess,lpRemoteCode,\
offset REMOTE_CODE_START,REMOTE_CODE_LENGTH,NULL
invoke WriteProcessMemory,hProcess,lpRemoteCode,\
offset lpLoadLibrary,sizeof dword * 3,NULL
mov eax,lpRemoteCode
add eax, offset _RemoteThread - offset REMOTE_CODE_START
invoke CreateRemoteThread,hProcess,NULL,0,eax,0,0,NULL
invoke CloseHandle,eax
.endif
invoke CloseHandle,hProcess
.else
invoke MessageBox,NULL,addr szErrOpen,NULL,MB_OK or MB_ICONWARNING
.endif
invoke ExitProcess,NULL

其中红色部分涉及到函数的使用。当编译器分析代码的时候,一旦它发现有函数名,那么编译器二话不说直接把函数名替换成函数代码所占据的地址空间的首地址。与变量的处理有稍许的差别,编译器不会主动的加上[]号。因此上面的红色部分可以直接改写成add eax, _RemoteThread - REMOTE_CODE_START。编译之后程序照样可以正确运行。在这样的情况下,你实在没有必要多此一举告诉编译器你不想加上[]号,因为在处理函数名方面,编译器原本就没打算帮你加上[]号。当然啦,为了增加代码的可读性,你可以加上offset伪指令,编译器也不会反对你这样做。

那么有读者会提问,为什么编译器在处理函数和变量的时候会有所差别呢?

是这样的,对于变量而言,它是用来存放数据的,因此它必须要占据地址空间。而对于函数来说,函数名仅仅只是某段代码首地址的代号,这就是他们之间的差别,还请读者自己去领悟他们两的异同。如果说编译器在处理函数的方式和处理变量方式一样的话,那么add eax, offset _RemoteThread - offset REMOTE_CODE_START
里的offset就不能省了,不然的_RemoteThread就会被编译器替换成此函数代码第一个字节的内容。很显然这不符合你编程的意图。总之,不管如何,你只要理解编译器在处理函数和变量的内部原理就行了。

汇编谈好了,我们来谈谈C语言,看看汇编和C语言编译器在对待函数和变量方面是不是一样。

我们来看看下面一段代码:

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

int random=0;

int main(int argc,char *argv[])

{

printf("main=0X%p\n",main);

printf("&random=0x%08X,random=%d\n", &random,random);

srand((unsigned)time(NULL));

random=rand();

printf("&random=0x08X,random=%d\n",&random,random);

getchar();

}

这段代码是我自己敲进来的,可能会有地方敲错,但是不影响代码的分析,所以我就没有核查。还请读者将就看看。你可以发现,红色部分正好是函数的使用,蓝色部分是变量的使用。这段代码运行之后的效果如下:

main=0x00401000

&random=0x00408A10,random=0

&random=0x00408A10,random=13155

OK,你现在可以把上面汇编的分析思路拿过来分析这段代码,令人兴奋的是,可以完全理解的通。当编译器分析printf("main=0X%p\n",main);这句话的时候,他二话不说直接吧main替换成此函数代码地址空间的首地址。当编译器分析printf("&random=0x%08X,random=%d\n", &random,random);这句话的时候他会把&符号理解成汇编里的offset。这样看来,汇编编译器和C语言的编译器在处理函数和变量方面手段都是一样的,处理方式也完全相同!!

好了,这节内容我们就谈到这里,如果读者能力全部理解的话,我相信你们再也不会被offset,addr,&,等等符号所困扰,并且还可以灵活运用,根据自己的喜好编写出符合自己理解角度的代码

另外,有读者会谈到“偏移”这样的概念。我额外说明下,现在我们的系统都在保护模式下,保护模式下的段的概念和实模式下段的概念已经完全不同!因此对于变量而言,其偏移全部针对0023段选择子描述的段,其函数地址的偏移全部针对001B段选择子描述的段。就应用程序编程而言,你完全没有必要理解什么是段选择子,因此你可以完全忽视“偏移”这个概念!这也就是为什么百度里很多朋友都把offset理解成“取地址”了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值