本文20236字数,聊到了字符数组相关的东西,阅读时常大概1天,下一篇会从指针和PE结构相关的知识出发。
可以加我VX: 我拉你公众号群: Get__Post
数据宽度
位,字节,字是计算机数据存储的单位,位是最小的存储单位,每一个位存储一个1位的二进制码,一个字节由8个位组成,而字通常为16,32或64个位组成。
位:
位是最基本的概念,在计算机中,由于只有逻辑0和逻辑1的存在,因此很多东西,动作,数字都要表示为1串二进制,例如1001,0000 1101等等。其中每一个0或1都是一个位,例如这个例子中的1000 1110共有8位,它的英文名字叫做bit,也就是8bit,是计算机中最基本的单位。
字节:
Byte,是由八个位组成的一个单元,也就是说八个位组成一个byte,也就是一个字节,在计算机科学中,用于表示ASCII字符,便是运用字节来进行表示字母和一些符号,例如A使用 0100 0001来表示。
字:
字(word) 代表计算机处理指令和数据的二进制数位数,是计算机进行数据存储和数据处理的运算的单位。
数据宽度有什么用?
在数学上我们是没有大小限制的,比如说我们没有最大数和最小数,但是在计算机中由于硬件的约束,数据是有长度限制的,就好比我们这边有一个瓶子,这个瓶子500毫升,我们给他装500毫升这是没有任何问题的,但是如果我们装600毫升的话,就会造成水溢出了,那么在计算机也是一样的,如果我们数据长度超过了规定的长度,那么超过的这些宽度的数据都会被丢掉。不管你存储的什么数据,在计算机中它最后都是以0和1的形式进行存储的。
比如说我们这里给一个eax寄存器中存储数据,eax是一个32位的寄存器,那么如果我们存储的数据超过32位的话,就会被丢掉。
那么我们先存储一个32位的数据可以看到是可以正常存储的。
这里存储的是0x12345678 这是一个16进制的数字,我们转换成二进制就变成了00010010001101000101011001111000 这里正好是32个0或1,这样是没有任何问题的。
那么如果我们存储的是0x12345678910的话那么肯定是不行的,因为这个瓶子只能存储32位的,超过了32位会溢出的。
可以看到虽然存储进去了,但是我们发现前面的值被丢弃了。这里是从右往左进行存的,可以看到123被丢掉了,这里是栈相关的,后面会说到。
那么假如我们的计算机中的数据宽度是4位的话,它能存储的数据如下:
在二进制中4位最小的是0000 最大的是1111
那么也就是说0000转换成16进制的话就是0
1111转换成16进制的话就是F 而转换成十进制的话就是15
那么也就是说是0-15
那么假如我们计算机中的数据宽度是5位的话,那么能存储的数据就是 00000 11111
那么转换成十进制的话就是0到31
那么我们32位的系统的话存储的数据就是:
0000 0000 0000 0000 0000 0000 0000 0000
1111 1111 1111 1111 1111 1111 1111 1111
那么转换成十进制就是0到4294967296 也就是2的32次方
通用寄存器
通用寄存器主要用于传送和暂存数据,它也可以进行逻辑运算,并且保存运算结果。除此之外他们还具有一些特殊的功能,汇编语言程序员必须熟悉每一个寄存器的一般用途和特殊用途,只有这样,才能在程序中做到正确并且合理的使用他们。
EAX:(针对操作数和结果数据的)累加器 ,返回函数结果
ECX:(字符串和循环操作数)计数器
EDX:(I/O指针)数据寄存器
EBX:(DS段中的数据指针)基址寄存器
ESP:(SS段中栈指针)栈指针寄存器
EBP:(SS段中栈内数据指针)扩展基址指针寄存器
ESI:(字符串操作源指针)源变址寄存器
EDI:(字符串操作目标指针)目的变址寄存器
那么我们前面说到过eax是一个32位的寄存器,那么如果我们想要去存储16位的数据,那么32位的寄存器后16位就被拆出来了。
就变成了这样如下图:
就比如说EAX拆出来之后 EAX的后16位就变成了AX,同样ECX,EDX都是一样的,他们的后16位被拆出来变成了16位的寄存器,那么我们可以给16位的寄存器进行存储数据。
比如说我们这里使用DOSBOX进行操作。可以看到这里我们使用的其实就是8086CPU,它其实本来就是16位的寄存器。
那么我们就可以往这16位里面存值。
16位的寄存器又分了两个8位的寄存器,这两个8位的寄存器,分为高位地址和低位地址。
AH是高位 AL是低位,那么我们可以给高位和低位进行存值。
那么一样我们可以给高位存24 低位存33,这样是没有任何问题的。
那么我们存储的数据宽度是需要一致的。
汇编指令
MOV指令:
mov指令其实就是将一个值给到寄存器中,前面也说到过,我们可以将16位或8位,32位的数据存储到寄存器中。
比如说如下汇编指令,这里ax是一个16位的寄存器,我们将3333这个值存储到ax这个寄存器中。
mov ax,3333
那么我们也可以存储到8位寄存器中,就比如ax分为两个8位寄存器,一个是AH一个是AL,AH为高位,AL为低位,所以我们可以使用如下汇编指令进行存储。
mov ah,33
mov al,44
ADD指令:
ADD指令是相加的意思,就比如说如下汇编指令。下面指令的意思就是将2356这个值存储到ax这个寄存器中,将3356这个值存储到bx这个寄存器中,然后使用ADD指令将2356和3356这两个值相加之后存储到AX这个寄存器中。
mov ax,2356
mov bx,3356
add ax,bx
SUB指令:
SUB指令和ADD是相反的,ADD指令是相加,SUB是减的指令。
比如说如下汇编指令:
这里的指令表示的是将33存储到ax寄存器的低8位寄存器,也就是AL,将22存储到BX寄存器的低八位,也就是22这个值,那么SUB指令就是将AL减去BL,然后存储到AL寄存器中。
mov al,33
mov bl,22
sub al,bl
ADN指令:
AND指令其实就是和的意思。在其他语言中就是如果两边判断都为真的话执行下面的代码,那么在汇编中都为1的时候才会为1,比如说如下汇编指令。
这里将5存储到了cl这个8位寄存器中,那么我们存储的5是一个16进制,那么它的二进制是0101,那么下一条汇编指令是and cl,2也就是说让2和5进行比较,2的二进制是0010。那么也就是说比较的其实是二进制,那也就是说0101和0010进行比较,那么我们来算一下,010的第一位是0,0010的第一位也是0,所以是算出来结果的第一位是0,第二位算出来的记过是0,因为0101的第二位是1,但是0010的第二位是0,只有这两个都为1的时候才会为1,所以最终算出来的结果是0000。
mov cl,5
and cl,2
OR指令:
前面的AND指令是只要我们2进制中需要两个值的二进制都为1的话才会为1,那么OR的话就是只要有一位为1那么就为1。
例如如下汇编指令。那么如下汇编指令计算出来的结果就是0006 转换成二进制就是0110
mov cl,6
or cl,2
内存寻址
通用寄存器是存在于CPU里面的,执行速度是非常快的,相较于内存的话相对比较慢,但是内存的空间很大,寄存器和内存没有本质的区别,都是用于存储数据的容器,都是定宽的(都是需要指定宽度的)。虽然CPU执行速度很快,但是我们总不能把数据全部放到CPU寄存器中,我们只有8个寄存器。
计算机中几个常用的计量单位是: BYTE,WORD,DWORD,QWORD
Byte 字节 = 8bit 8位
WORD 字 = 16bit 16位
DWORD 双字 = 32bit 32位
QWORD = 64bit 64位
内存的数量是很庞大的,无法每个内存都起一个名字,所以用编号来代替,我们称计算机CPU是32位或者64位,有很多书上说之所以叫做32位计算机时因为寄存器的宽度时32位,这是不准确的,因为还有很多寄存器时大于32位的。
在CPU中有32跟线,可以理解为有32个0或1,那么它每次变化一次都能代表一个值,这个值就是一个编号,我们可以通过这个编号找到所对应的值。每一块内存都有一个编号。内存中的编号是以字节为单位的。那么一个编号对应1个字节。这里需要注意的是在OD或者x32dbg中每4个字节起一个编号
那么我们既然知道了一个内存编号对应一个字节,那么我们是不是可以最大的那个内存编号转换成10进制,然后看看最大的寻址范围是多大。
FFFFFFFF + 1 因为前面还有一个0 = 1 0000 0000(16进制)
转换成十进制: 4294967296
然后除以1024 最后会变成4G,这就是为什么有时候别人说内存最大的寻址范围是4G.
内存格式:
CS,DS,SS,ES:在用户层只是起到寻址的作用,在内核层涉及到段,页会有其他的用途。
cs:代码段
ds:数据段
ss:堆栈段
es:附加段
向内存中写入数据:这里的意思就是将eax这个寄存器中的数据存储到内存中。这里需要注意的是这里使用的是dword,dword是一个32位的内存,而eax也是一个32位的寄存器,所以是可以写入的,但是如果换成byte或者16位的话是不行的。
这里的[0x19FF74]表示的是这一块的内存地址。
也就是说将eax这个寄存器中的数据存储到[0x19FF74]这块内存地址中。
mov dword ptr ds:[0x19FF74],eax
那么我们一样可以反过来,将内存的值存储到寄存器中
这里就是将[0x19FF74]内存中的值取出来,然后放到ax这个16位寄存器中。
mov ax,word ptr ds:[0x19FF74]
那么我们也可以直接将立即数存储到内存中去。
mov dword ptr ds:[0x19FF74],0x6666666
那么也可以跟之前的一样,add也是一样的。
这个汇编语句的含义就是将eax这个寄存器的值和内存的值相加之后然后存储到[0x19FF74]这块内存中。
add dword ptr ds:[0x19FF74],eax
我们前面说每一个编号对应一个字节,也就是8位,那么为什么我们前面存储的时候直接可以存储32位,也就是4字节呢?
那么我们来看一下,在内存中它会4个字节起一个编号例如:
可以看到是008FFC04下面是08了,就不是05,所以是4个字节起一个编号。
那么我们会发现在堆栈窗口这里008FFC04存储的值是00670075,但是在内存中显示的是75006700,这是因为在数据窗口中高位在后低位在前,也就是说从后往前读就是00 67 00 75 即可.
然后如果我们想存储数据的话,byte word dword表示我们影响的内存编号。
例如我们向008FFC04内存编号中存储一个字节的数据。
我们可以看到从00670075变成了0067000A,只修改了75这个值,那么也就是说75所代表的内存编号就是008FFC04,那么70的内存编号就是008FFC04 + 1的内存编号。
mov byte ptr ds:[0x008FFC04],0xA
那么比如说我们向008FFC04 + 1 的编号存储一个值。
我们发现从0067000A值变成了00670B0A 那么再次说明,其实0B表示的内存编号就是0x8FFC05。
mov byte ptr ds:[0x8FFC05],0x0B
Lea指令
Lea指令是获取内存地址的指令,比如说我们想获取一块内存的地址就使用lea指令即可。
比如如下汇编:
这里指令的意思就是,取ecx这个寄存器中的值,去找这个值对应的地址,然后存储eax这个寄存器中。这里一般不会是一个立即数。
mov eax,dword ptr ds:[0x006FF98C] //将这个地址中的值移动到eax寄存器中
lea eax,dword ptr ds:[0x006FF98C] //将这个地址移动到eax寄存器中
//这里需要注意的是一个是移动内存编号所对应的值,一个是移动内存编号。
那么如果是这样呢?其实都是一样的。
mov eax,0x006FF98C
lea eax,dword ptr ds:[eax]
这里lea主要是取得是地址。
如下例子:这样最后存储到eax寄存器中的是值,而不是内存编号。
mov eax,0x006FF98C
mov eax,dword ptr ds:[eax]
标志寄存器
在X86架构中,标记寄存器主要有3种作用:
存储相关指令执行后的结果,例如CF,PF,AF,ZF,OF标志位
执行相关指令时,提供行为依据,例如执行JE指令时会读取ZF的值,来决定是否进行跳转
控制CPU的工作方式,例如IF,VM,TF等标志位
我们在OD或x32dbg中可以看到工具已经给我们拆好了。
这里EFLAGS是246。
我们可以来拆一下,我们拆出来对应的就是上面那张图:
246转换成二进制就是 0010 0100 0110
那么也就是说CF这个位是0,上面那张图中的灰色是我们无法控制的,是默认的,为1,PF为1,最后依次类推即可。
CF:
CF是进位标志寄存器,如果运算的结果的最高位产生了一个进位或借位的话,那么其中的值就为1,否则为0。
如下汇编是不会导致CF标志寄存器进位的,因为最前面的6才是最高位。
mov eax,0x66FFFFFF
add eax,1
那么如下汇编就会导致进位:
mov al,0xFF
add al,1
PF:
奇偶标志PF:奇偶标志PF用于反应运算结果中"1"的个数的奇偶数。
如果"1"的个数为偶数,则PF的值为1,否则为0。
例如如下汇编:
3的二进制是0011,做完运算之后为6,二进制是0110,有两个1,所以为偶数,PF标志寄存器为1。
mov AL,3
ADD AL,3
ADD AL,3
这里有一个坑,比如说我们写一个803写入到AX16位的寄存器中,然后让它加1。
我们会发现PF标志位并没有变1。
这是为什么呢?这是因为它只会看最低有效字节,也就是说当803加1之后变成804之后,他只会看04转换成二进制之后有没有偶数的1,如果没有的话那么就不会变成1。
ZF:
零标志ZF,零标志位ZF用来反映运算结果是否为0,如果运算的结果为0,则其值为1,否则其值为0,在判断运算结果是否为0时,可使用此标记位。
如下指令会将eax寄存器清零。这里就是将eax进行异或,然后将异或的值放到eax里面。同时这里的ZF标志寄存器变为1。
xor eax,eax
SF:
符号标志位SF:符号标志SF用来反映运算结果的符号位,他与运算结果的最高位相同。
就比如说7F转换成二进制是0111 1111,那么给他加2的话他的最高位也就是0就会。
mov AL,7F
add AL,2
OF:
O位主要看的是有符号数,而C位主要看的是无符号数。
如下图:
就拿出来第一个来看一下吧,AL是一个8位寄存器,里面存储8这个值,那么8的话再8位寄存器中如果我们看作是有符号数的话那么它就是正数,那么再加8,也就是再查8位的话没有超过7F自然是不会溢出的。
那么如果我们看作是无符号数的话,只要不超过FF 就不会溢出。
那么第二个也是一样的,如果看作有符号数的话,0FF是负数,然后加2的话,也就是负数加正数,是不会溢出的。
那么如果是无符号数的话,你FF是最大的值了,那么再加2的话就会溢出了,因为不能大于FF,如果大于FF的话,作为无符号数就会溢出。
第三个7F是正数的最大数,那么再加2的话,作为有符号数的话是肯定溢出的,因为加2的话就超过7F了,那么作为无符号数的话是不会溢出的,因为不超过FF。
那么无符号所对应的标志位寄存器是CF位,有符号数对应的标志寄存器是OF位。
ADC指令:
ADC是带进位加法。
ADC AL,CL
SBB指令:
SBB指令是带借位减法。
XCHG指令:
XCHG指令是交换的意思:
xchg dword ptr ds:[0x19ff78],eax
xchg byte ptr ds:[0x194456],AL
MOVS指令:
移动数据 内存-内存
MOVS BYTE PTR DS:[EDI],BYTE PTR DS:[ESI] //这里表示的是ESI中的内存地址所对应的值移动到EDI地址所对应的值,需要注意的是数据宽度需要是一样的。可以简写为MOVSB
MOVS WORD PTR DS:[EDI],WORD PTR DS:[ESI] //MOVSW
MOVS DWORD PTR DS:[EDI],DWORD PTR DS:[ESI] //MOVSD
STOS指令:
将AL/AX/EAX的值存储到[EDI]指定的内存的单元。
//如果是BYTE那么就是AL,如果是WORD那么就是AX,如果是DWORD那么就是EAX。
STOS BYTE PTR ES:[EDI] 简写STOSB
STOS WORD PTR ES:[EDI] 简写STOSW
STOS DWORD PTR ES:[EDI] 简写STOSD
我们再次执行STOSD,我们发现地址从006FFCE0变成了006FFCDC 发现16进制减了4。并且006FFCE0到006FFCDC 这一块堆栈空间被填充了ccccc....
REP指令:
REP指令表示的是根据ECX寄存器中指定的次数重复执行字符串指令。
这里就跟循环差不多。
mov ECX,10
REP MOVSD //这里的指令意思就是循环16次MOVS DWORD PTR DS:[EDI],DWORD PTR DS:[ESI] 这条指令
这里比较的是ebp和esp的值是否相等,如果相等的话,那么会将标志寄存器的ZF标志位变成1这个值。
这八个寄存器是包含在EFLAGS中的。
在16位模式下,标志位寄存器的名字为FLAG 寄存器的大小为16位
在32位模式下,标志位寄存器的名字为EFLAG,寄存器的大小是32位
在64位模式下,标志位寄存器的名字为RFLAG,寄存器的大小是64位
EFLAG寄存器的每个位都有不同的用途,灰色的部分为保留位。
这里我们可以看到EFLAG的值是246,这里转换成二进制就可以对应如上图。
栈
一个应用程序中有各个分区我们可以分为如下图:
这里R表示读的权限,W表示可写的权限,X表示可执行的权限。
一般栈中存储的是局部变量,参数,临时数据。例如如下C代码:
这里的int i 就是局部变量,函数内声明的是局部变量,那么如果在函数外声明的话就是全局变量。注意这里局部变量存储在栈中。
那么参数的话就是给函数传递的参数,如下C代码:
这里的int i 和 int j就是函数参数
void func(int i,int j){
}
int main(){
}
如上就是存储在栈中的,并且是可读可写的 RW
当我们去调用malloc或者new函数的时候它申请出来的空间是在堆里面的
并且也是可读可写的 RW
如上是存储在堆中的
常量区的话里面存储的都是常量的值,相比变量来说常量是不会改变的,就是将一个固定的值设置为常量
堆栈
比如说我们想在内存中存储一些数据的话,比如说我们存储到这个0x19ff74这个内存地址上面。
如下图:
我们在EBX中存储的内存地址,而这个内存地址指向的数据是0x12345678
那么我们如果需要取这条数据的话,汇编指令应该是这样的:mov eax,dword ptr ds:[ebx]
那么如果我们要通过EDX取0x12345678这条数据的话,那么我们就变成了 mov eax,dword ptr ds:[edx - 4] 这里为什么要减4?
因为数据在0x19ff74这个内存中,而EDX寄存器存储的内存地址是0X19ff78,所以减4的话就变成了0x19ff74。
这里就引出来了栈顶和栈底的两个概念。
栈顶对应的ESP,栈底对应的是EBP
JMP指令:
可以看到如下图已经可以跳转到0x00131897这里了,可以看到ESP和EBP是没有任何变化的,但是我们发现EIP变化了,所以本质上是修改EIP的。
jmp 0x00131897
CALL指令:
CALL指令会改变堆栈结构,我们定位到CALL指令这里,首先这里的ESP是00CFF680,EBP是00CFF77C。当我们F7进入函数之后,会发现ESP和EBP改变了。
可以看到进入之后ESP和EBP发生改变了。我们发现现在的ESP是00CFF67C,EBP的地址是00CFF77C,可以发现在我们没有调用CALL指令之前ESP的值是00CFF680,调用之后变成了00CFF67C,也就是说减了4,进行了提栈的操作。
如下图:
当内存提4个字节的时候,会减4,因为它是从高字节往低字节的。
我们程序是一行一行往下执行的,除非遇到JMP等其他的指令。
就比如说当我们一个函数执行完成之后,肯定要返回我们的main函数,也就是我们主程序继续执行的。
也就是说当我们执行完CALL指令之后,它会将下一次执行的地址存储起来。
如下图:
也就是说当我们去调用CALL指令的时候,它会将下一条要执行的汇编指令存储到ESP寄存器中的地址所对应的值,在如下图中的地址是00131AC0
我们可以跟进去CALL。
进入到CALL之后,我们发现在ESP寄存器中的地址是003FFC74,而它对应的值是:00131AC0
也就是说调用CALL指令的时候会发生两个变化,一个是他会将ESP提升4个字节,并且会将返回的地址压入到堆栈中。
ret指令:
ret指令的意思就是既然我们需要进入到函数里面,那么肯定是需要返回的。
test指令:
test指令是对两个数进行相与操作的。
一般用于test指令的话就是判断寄存器的值是否为0。
test eax,eax
压入堆栈
压入堆栈的意思就是往堆栈中存储数据。
1.压入数据
第一种方式:
这里就给内存006F0056地址存储了0x123456这个值。
我们可以发现已经将值存到004FFC94这个内存地址里面了,但是我们发现ESP的是004FFC98,那么那么也就是说我们将数据存储到堆栈外了,所以我们需要给ESP减4,让数据存储到堆栈内。
mov dword ptr ds:[0x004FFC94],0x123456
这里进行减4。
sub ESP,4
当我们给ESP减4之后ESP寄存器的值变成了004FFC94,正好对应的就是我们的数据,就说明我们存储的数据已经在堆栈内了。
第二种方式:
第一种方式是先提栈的方式,那么第二种方式就是先将ESP的值减4,然后再存入数据。
sub esp,4
mov dword ptr ds:[0x004FFC90],0x123456
如上这两种方式都可以。
2.读取数据
我们可以直接使用ESP进行取数据,因为栈顶ESP对应的就是我们存进去数据的地址值。
mov eax,dword ptr ds:[esp]
也可以使用栈底去取数据。
mov eax,dword ptr ds:[ebp-4]
那么同样也可以使用esp去取下面的数据,例如:
mov eax,dword ptr ds:[esp+4]
原本的ESP是004FFC90这个内存地址,那么加4之后就变成了004FFC94这个内存地址了,就相当于从这个内存地址中取值然后赋值给eax这个寄存器。
如上的指令是不太常用的,我们常用的指令是push 和 pop指令。
push指令:
我们现在的栈顶(ESP)的地址值是004FFC90。
push 0x123456
当我们运行push指令的时候,ESP的值会减4,就好比我们上面直接使用SUB指令给它减掉一样。
如下图:
不仅ESP减4,并且将我们的值存储到了eax寄存器中。
pop指令:
pop指令和push指令是相反的,例如如下汇编指令:
这个指令的意思是压栈的意思,push指令是提栈,这里会将我们堆栈中的数据存储到eax寄存器中,并且会进行压栈操作。
pop eax
以看到ESP的值从004FFC8C变成了004FFC90,就相等于给ESP加4。
那么我们思考一个问题:当使用push指令的时候栈顶一定会减4吗?
答案是不一定,如果是我们push的是一个BX,BX是一个16位的寄存器,那么栈顶也就是ESP只会减2。
如下图push BX;
没有执行之前:
执行之后: 012FE978变成了012FE97E 减2了。
但是我们能不能push al呢?我们都知道al是8位寄存器,答案是不行的。
那么我们能不能push一个内存中的值呢?这里我们push的是一个dword也就是双字。
可以发现值从012FE97E变成了012FE97A,栈顶减了4。
那么我们push一个字也就是word的话 栈顶会减2。
可以看到值从012FE97A变成012FE978 减了2。
那么如果我们POP AX的话,那么栈顶的值就会加2。
那么既然可以pop到寄存器中那么是不是也可以pop到内存中呢?比如说我们pop到一个32位或16位的内存中是不是也可以呢?
pushad指令:
这里我们除了esp和ebp之外我们将其他寄存器的值改为了1,2,3,4,5,6,7,8.
然后我们执行一下pushad指令。
如上图可以看到当我们执行pushad指令的时候它会将我们寄存器中的值压入到栈中。
popad指令:
popad就是恢复的意思,比如说我们将里面的值修改成任意的值。
然后我们执行popad指令,会发现他会恢复到我们之前所存储到寄存器中的值。
JCC
jmp指令:
jmp指令是修改EIP的值的,需要注意的是它并不是无条件跳转,而是修改EIP的值,CPU执行的跳转。
例如跳转到00981845这个地址。
还有就是jmp不会更改堆栈结构。
jmp 0x00981845
CALL指令:
call 0x981852
我们会发现使用CALL指令的时候,ESP栈顶发生了变化。
那么JMP和CALL指令的的共同点就是都是修改了EIP,不同点是JMP指令不会导致堆栈的变化,而CALL指令会导致堆栈的变化。
那么我们会发现在ESP中地址存储的值是我们的返回地址,也就是在CALL指令后的指令。
这个00981850值就是在CALL指令后的下一跳地址。
那么我们在想它是怎么知道我们下一跳地址是00981850的呢?是怎么算出来的呢?
我们注意到这一行有5个字节,0098184B + 5 正好等于00981850。
CMP指令:
cmp指令,只改标志寄存器的值,不会更改原来的值。cmp指令就是比较两个值是否相等。
TEST指令:
该指令在一定程序上和CMP指令是类似的,两个数值进行比较,结果不保存,但是会改变相应标志位的值。
一般用于判断某个寄存器里面是否是0。
test eax,eax
比如eax中的值是:00000010
那么他会进行一个与操作:
也就是说与操作的话只有全是1的时候才会是1,那么也就是说eax中的值全是0的情况下,标志位的值才会改变。
可以发现ZF标志位发生了改变。
JE/JZ
je和jz是相同的两个指令,结果为零则跳转(相等时跳转) 主要看的是ZF标志位,需要记住的是他要cmp,test等等没有任何关系,它只认标志位,只要所对应的标志位是1,那么我就跳。
mov eax,200
mov ebx,200
cmp eax,ebx
je 0x0098184C
执行je之后:
发现跳转到了0x0098184C这里,只需要记住它是根据ZF这个标志位进行跳转的即可。
JNE/JNZ
jne和jnz指令是结果不为零的话跳转,和上面是相反的,它所对应的标志位是ZF=0
那么也就是说ZF等于0的时候进行跳转。
jne 0x00981851
堆栈图
首先ESP和EBP的值分别是00D7FB84,00D7FC50。分别对应栈顶和栈底。
首先push指令,会提栈4个字节,那么ESP的值减4,并且将数据压入到栈中。
这里有两个push的指令,那么也就是说ESP的值减8。ESP减8变成了D7FB7C。
堆栈图:
那么接下来调用call指令,当调用call指令的时候,会有提栈操作,ESP减4。此时的ESP:D7FB7C
F7跟进去之后ESP的值变成了00D7FB78。也就是说D7FB7C减4的值。并且会将下一次执行的地址压入进去。
堆栈图:
接着push ebp,那么ESP减4,进行提栈。然后将ebp压入到栈中。那么也就是说00D7FB78 减4变成了00D7FB74
堆栈图:
接着mov ebp,esp sub esp,cc
这里将esp的值给了ebp,将esp的值减掉cc。注意这里mov是不会导致堆栈变化的。
首先esp给了ebp这两个值变成一样的了。
堆栈图:
再将esp减cc,也是一个提栈的操作。减掉CC之后ESP变成00D7FAA8。
堆栈图:
紧接着就是三个push指令:
push ebx
push esi
push edi
那么也就是说提栈的话 ESP-C,那么现在的ESP就变成了D7 FA9C
堆栈图:
接着通过lea指令获取内存地址。并放到edi这个寄存器中。
之后就是mov ecx,3和mov eax,cccccccc
然后就是stosd指令了,它的意思是将循环EAX值存储到EDI指定的内存中,执行ecx中存储的次数,每次执行一次ecx的值-1 ,这里需要注意的是每次执行完成之后EDI + 4,因为标志位D,里面存储的值是0,如果是1的话那么就减4。
其实意思就是将EAX的值放到EDI中,循环的次数由ecx的次数决定的。
如下图就相当于将cccccccc存储到00D7FB68,循环3次。
堆栈图:
接下来就是我们程序的代码了。
首先将ebp+8变成了D7FB7C,然后取这个内存地址中的值放到eax寄存器中。也就是1这个值。
然后将ebp加c取出这个内存地址的值之后和eax寄存器中的值进行相加。
也就是说00D7FB80这个内存地址的值 + 1 然后存储到eax这个寄存器中。
紧接着将eax中的值放到ebp减8这个内存地址中。也就是将eax中的值存储到了D7 FB6C这个内存地址。
然后再将ebp-8内存地址的值存储到eax寄存器中。
那么就变成了这个值。D7FB6C,那么它对应的值就是3
然后就是pop指令,弹出的意思。
pop edi
pop esi
pop ebx
就相当于给这三个进行了还原操作。
然后紧接着add操作,将esp的值和cc进行相加之后存储到esp中。
esp此时的值是00D7FAA8 加cc之后变成了00D7 FB74,这里是就相当于降栈的操作。
堆栈图:
然后cmp进行判断ESP和EBP的值是否一样,如果是一样的话,ZF标志位变成1.
然后就是mov esp,ebp 将ebp给了esp。
最后就是pop ebp,将ebp的值进行还原。这里就相当于 pop eip。
注意:这里我重新调试了,所以ESP和EBP的值不一样了。
出来之后他会有一个add的操作,因为我们进去的时候值和返回的值是不一样的,所以它需要将我们的值恢复成原来的样子。因为是需要堆栈平衡的。
void func() {
int i = 1;
int j = 2;
int k = i + j;
printf("%dn", k);
}
int main()
{
func();
return 0;
}
如下图反汇编:
可以看到这里将1和2 存储到了ebp-8和-14的内存地址中,然后调用add指令进行相加等等。
以上是局部变量的情况。
那么全局变量的话如下C代码:
int i;
int j;
void func() {
int i = 1;
int j = 2;
int k = i + j;
printf("%dn", k);
}
void func1() {
i = 1;
j = 2;
j = i + j;
}
int main()
{
func();
return 0;
}
我们首先来看下i和j的地址。
i的地址是0x0009A138
j的地址是0x0009A13C
如上说明他不是通过EBP和ESP进行寻址的,而是通过地址直接寻址的。
通过EBP和ESP寻址的都是存储在栈中的。
那么如果以全局变量的形式都是存储在堆中的。
那么如果我们全局变量初始值的话,那么它的内存值是直接有值的。
那么我们来看下有参数和没有参数的。
如下C代码:
// 堆栈图1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
int i = 5;
int j = 7;
void func() {
int i = 1;
int j = 2;
int k = i + j;
printf("%dn", k);
}
void func1() {
i = 1;
j = 2;
j = i + j;
}
void func2(int i,int j) {
j = i + j;
}
int main()
{
func();
func1();
func2(1, 23);
return 0;
}
可以看到反汇编之后首先push压入栈,然后才调用的func2函数,对比之前的func和func1,多了两个push的操作。
我们跟进去call指令。
我们发现这里对ebp进行了+8操作,在之前的函数中都是ebp减4然后进行寻址的。这是有参数的形式。
堆栈图:
那么我们就可以总结出来只要是在call指令之前进行push的都是参数传递的,而在call指令里面EBP+8的都是全局变量,在call指令中ebp减8的都是局部变量,局部变量存储在栈中,全局变量存储在堆中。
反调试
那么我们在想一个函数中嵌套一个函数的话,那么我们是跟进去还是不跟进去呢?
这里的意思是如果我们将函数的返回地址修改掉了话,那么就能起到一个反调试的作用。
比如说如下C代码:这里我们主要看func3这个函数。
// 堆栈图1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
int i = 5;
int j = 7;
void func() {
int i = 1;
int j = 2;
int k = i + j;
printf("%dn", k);
}
void func1() {
i = 1;
j = 2;
j = i + j;
}
void func2(int a,int b) {
j = a + b;
}
void func3() {
func();
func1();
func2(1,2);
}
int main()
{
func();
func1();
func2(1, 23);
func3();
return 0;
}
放到x32dbg中查看:
就比如说我们将这三个call指令中的函数返回地址修改一个不知道的地址那么就会起到一个反调试的作用。
如下图我们将函数的返回地址的值改为其他的值。
那么当别人去调试我们程序的时候,当他进入到函数之后返回的地址值就不是正常的了。
可以看到我们F8出去的时候跳到了其他地方。
C语言
函数
无参函数:
无参函数是没有返回值的。
void 函数名(){
//代码
}
反汇编:
跟进:
可以发现虽然我们在函数中没有写任何功能的代码,但是CPU也帮我们做了很多事情。
反汇编分析:
反汇编分析1:
编写一个函数能够对任意两个数字实现加法,并分析函数的反汇编。
//实现对任意两个整数进行加法操作
int Plus1(int x, int y) {
return x + y;
}
//程序的入口
int main(int argc,char * argv[])
{
Plus1(1,3);
return 0;
}
反汇编分析2:
编写一个函数,能够对任意3个整数实现加法,并分析函数的反汇编(要求使用上一个函数)。
int Plus2(int x, int y, int z){
int t;
int r;
t = Plus1(x,y);
r = Plus1(t,z);
return r;
}
int main(){
Plus2(1,3,3);
return 0;
}
裸函数:
裸函数定义:
void __declspec(naked) Plus() {
}
int main(){
Plus();
}
当我们去运行的时候会发现报错了,我们跟进去反汇编发现里面并没有生成的反汇编代码。
可以发现这里全是int 3。
我们在前面都知道一个空参数它是有编译器给它生成的汇编代码的,但是裸函数是没有的,当我们执行下去的时候会报错。
在空函数中有一个关键点就是Ret,那么我们就需要给裸函数也加上ret返回的汇编代码。
这样的话就可以正常运行了。
void __declspec(naked) Plus() {
__asm{
ret
}
}
int main(){
Plus();
}
理解:
如果是一个普通的函数编译器会帮我们做很多事情,那么如果加上 __declspec(naked)关键词的话,就变成了裸函数,就相当于告诉编译器你们别管我了,我自生自灭。
裸函数例1:
利用裸函数写一个两个数字相加。
int __declspec(naked) Plus(int x,int y) {
__asm {
//保留调用前的堆栈
push ebp
//提升堆栈
mov ebp, esp
sub esp, 40
//保留现场
push ebx
push esi
push edi
//填充缓冲区数据
mov eax, 0xCCCCCCCC
mov ecx, 0x10
lea edi, dword ptr ds : [ebp - 0x40]
rep stosd
//函数功能
mov eax, dword ptr ds : [ebp + 0x8]
add eax, dword ptr ds : [ebp + 0xC]
//恢复现场
pop edi
pop esi
pop ebx
//降低堆栈
mov esp, ebp
pop ebp
ret
}
}
void Plus1() {
}
int main()
{
int c = Plus(1,2);
c);
return 0;
}
反汇编:
调用约定
常见的几种调用约定,调用约定规定了我们的参数是如何传递的,__cdecl是C/C++默认的。
调用约定 | 参数压栈顺序 | 平衡堆栈 |
---|---|---|
__cdecl | 从右至左入栈 | 调用者清理堆栈 |
__stdcall | 从右至左入栈 | 自身清理堆栈 |
__fastcall | ECX/EDX传送前两个,剩下的从右往左入栈 | 自身清理堆栈 |
__cdecl
int __cdecl Plus1(int x,int y) {
return x + y;
}
int main(){
Plus1(1,2);
}
__cdecl的参数是从右到左进行压栈的。
可以看到先压的2,再压的1,那么谁来平衡堆栈呢?谁调用了我整个函数谁就来平衡堆栈,也就是说我们在main函数中调用的,main函数负责平衡堆栈。
__stdcall
int __stdcall Plus2(int x, int y) {
return x + y;
}
int main(){
Plus2();
}
stdcall的参数是从右往左入栈的,和__cdecl不同的是它是自己平衡堆栈的。
我们可以看到在外面是没有add esp,8 这行代码的,也就是说它不是在外面平衡堆栈的,我们跟进去。
函数里面可以看到ret 8,这里可以看成两条指令,一个是ret,一个是esp+8,这种称为内平栈。
__fastcall
__fastcall它会将前面两个参数先传递给EAX和EDX寄存器,剩下的参数从右往左入栈。这里需要注意的是,如果只有两个参数的话因为是传递给寄存器的所以是不需要平栈的,但是如果超过两个参数的话那么就需要内平栈了,如下图其实是不需要平栈的,因为参数传递给了寄存器,并没有push。
可以看到函数里面并没有平栈操作。
如果我们传递2个以上的参数的话,那么就需要平栈了。
函数里面:
程序入口
程序入口其实并不是我们所说的main函数,在main函数之前做了很多初始化的事情。
可以看到在调用main函数之前做了很多初始化的操作。
main函数被调用前要先调用的函数如下:
GetVersion()
_heap_init()
GetCommandLineA()
_crtGetEnvironmentStringsA()
_setenvp()
_setargv()
_cinit()
那么这些函数调用完成之后才会调用main函数,根据main函数调用的特征,将三个参数压入栈中作为函数参数。
那么我们如何才能找到这个程序入口呢(Main函数的入口)?
接下来我们来找一下:
首先可以看到在main函数之前执行了这几个函数。
首先执行的是common_main() 函数,那么我们将exe托到x32dbg里面。
可以发现第一个函数我们已经找到了,然后我们F7跟进去。
那么第二个函数是scrt_common_main_seh(),我们可以发现已经找到了。然后我们F7跟进去。
跟进去之后接下来需要找invoke_main()函数,在这个函数中调用了我们的main函数。
在这里的话也找到了invoke_main函数,然后我们F7跟进去就是我们的main函数了。
main函数内部:
数据类型
数据类型的三个要素:
1.存储数据的宽度
2.存储数据的格式
3.作用范围(作用域)
整数类型:
char short int long
char 8BIT 1字节 byte
short 16Bit 2字节 word
int 32BIT 4字节 dword
long 32BIT 4字节 dword
调试代码:
void Plus1() {
char i = 0x12345678; //byte
short x = 0x12345678; //word
int y = 0x12345678; //dword
}
int main()
{
Plus1();
}
这里的int其实在汇编里面就是dword,32位,4个字节。
char 就是1个字节,8位。
short 就是2个字节 16位。
如下图:
有符号和无符号
这里无论是有符号还是无符号的在内存中存储的值都是一样的。
void Plus1() {
//默认就是有符号
char i = 0xFF;
unsigned char k = 0xFF;
}
int main()
{
Plus1();
}
还有一点就是我们会发现打印出来的这两个值是不一样的。
有符号的打印出来时-1,无符号的打印出来时255,那么也就是说在你计算的时候需要指定有符号还是无符号,因为这两个的结果是截然相反的。
总结:
1.无论是有符号数还是无符号数在内存中存储的方式是完全一样的。
2.在做运算的时候是需要注意是有符号数还是无符号数。
浮点类型:
float double
浮点类型如何存储在内存中的呢?
留坑
作业:
使用汇编代码编写如下程序:
int plus(int x,int y,int z){
int a = 2;
int b = 3;
int c = 4;
return x + y + z + a + b + c;
}
汇编代码:
#include <iostream>
int __declspec(naked) Funcation(int x,int y,int z) {
__asm {
//保存原来的栈底
push ebp
//提升堆栈
mov ebp,esp
sub esp,40
//保留现场
push ebx
push esi
push edi
//向缓冲区填充数据
3
mov ecx,10
mov eax,0xCCCCCCCC
lea edi,dword ptr ds:[ebp-0x40]
rep stosd
//函数功能
ebp -4开始 参数是从ebp + 8开始的 ebp+4里面存储的是eip, ebp:原来的栈底。
mov dword ptr ds:[ebp-0x4],2
mov dword ptr ds : [ebp-0x8],3
mov dword ptr ds : [ebp-0xc],4
mov eax,dword ptr ds:[ebp+0x8]
add eax, dword ptr ds : [ebp + 0xc]
add eax, dword ptr ds : [ebp + 0x10]
add eax, dword ptr ds : [ebp - 0x4]
add eax, dword ptr ds : [ebp - 0x8]
add eax, dword ptr ds : [ebp - 0xc]
//恢复现场
pop ebx
pop esi
pop edi
//恢复堆栈
mov esp,ebp
pop ebp
ret
}
}
int main()
{
2, 3);
}
英文字符存储到内存中
void Plus(){
char c = 'A';
}
int main(){
Plus();
return 0;
}
发现A字符存储到内存中是16进制的41。
那么16进制的41转换成十进制是65。
那么65对应的Ascii码就是'A'。
如下图就是标准的Ascii码表,标准的Ascii码只占7位,它的最高位永远是0。
中文字符存储到内存中
那么存储中文的话就不能使用ASCII码了,中国的专家在ASCII的基础上拓展了GB2312来表示汉字,使用2个字节进行存储,也就是两个ASCII码。
void Plus() {
char* x = "啊";
}
int main()
{
Plus();
return 0;
}
可以看到内存中所存储的值是a1b0。
内存图
全局变量和局部变量
这里的g_n存储在全局变量区,EXE编译完成之后地址就已经确定下来了,是否有值取决于声明的时候是否给定了初始值,如果没有,默认为0,只有程序重新启动的时候,地址才会变化。
那么当调用Function函数的时候,这里会将Function函数放到存储到代码区中,j和o作为参数存储在堆栈中,那么其中的x和y作为局部变量也存储到堆栈中,包括返回的临时值也存储在堆栈中。
int g_n = 10;
int Function(int j,int o){
int x = 2;
int y = 3;
return g_n + x +y;
}
全局变量的值可以被所有函数所修改,里面存储的是最后一次修改的值。
全局变量所占内存会一直存在,直到整个进程结束。
全局变量的反汇编识别:
mov 寄存器,byte/word/dword ptr ds:[0x12345678]
通过寄存器的宽度,或者byte/word/dword来判断全局变量的宽度。
全局变量就是所谓的基址
局部变量的特点
1.局部变量在程序编译完成后并没有分配固定的地址。
2.在所属的方法没有被调用时,局部变量并不会分配内存地址,只有当所属的程序被调用的时候才会在堆栈分配内存。
3.当局部变量所属的方法执行完毕后,局部变量所占用的内存就会变成垃圾数据。
4.局部变量只能在方法内部使用,函数A无法使用函数B的局部变量。
5.局部变量的反汇编识别:
ebp-4
ebp-8
ebp-C
变量的类型转换:
例如如下代码:
void function(int x, int y) {
char a = 10;
int i = a;
}
int main()
{
function(4,5);
return 0;
}
我们先来看几行汇编语句:
movsx先符号位扩展,再传送。
mov AL,0XFF
movsx CX,AL
mov AL,80
movsx cx,AL
就比如说我们向AL这个8位寄存器中存储0XFF,转换成二进制就是1111 1111。
那么吧AL寄存器中的值存储到CX这个16位的寄存器中,那么就需要补符号位。就变成了 1111 1111 1111 1111
也就是FFFF。
如下图:
那么比如说我们向AL这个8位寄存器中存储0XC的话,转换成二进制就是0000 1100
那么吧AL寄存器中的值存储到CX这个16位的寄存器中,那么就需要补符号位了,就变成了0000 0000 0000 1100如下图:
movzx 先零扩展再传送。
例如:
mov AL,0XFF
movzx cx,al
可以发现前面使用0补齐了前面的8位。
那么从大的类型往小的类型转换的话会进行截取:
void function(int x, int y) {
int ni = 0x12345678;
short si = ni;
char ci = ni;
}
int main()
{
function(4,5);
return 0;
}
如下图:可以看到原本存储到堆栈的值是0x12345678,然后我们将这个值存储到AX这个16位的寄存器中发现只存储了0x5678,这就是进行了截取操作。
那么如果存储到AL呢?
AL是8位寄存器,那么存储进去的值就是78了。
函数参数的反汇编
如何判断函数有几个参数:
步骤一: 观察调用处的代码
push 3;
push 4;
push 5;
call 0x12345678
步骤二:找到堆栈平衡的代码继续论证:
call 0x12345678
add esp,0Ch
或者函数内部:
ret 4/8/0xC/0x10
最后两者一综合,函数的参数基本个数可以确定。
参数未必都是通过堆栈,还有可能通过寄存器,比如说fastcall,内平栈,如果前面有两个值给了EAX和EDX寄存器,那么接下来的参数才会进行push,所以在确定函数参数之前还要进行论证。
IF逆向分析
#include <iostream>
int g_n = 10;
void Funtion(int x,int y) {
if (x > y) {
g_n = x;
}
}
int main()
{
Funtion(2,3);
return 0;
}
这里jle是小于或等于的意思,也就是说当eax小于或等于ebp+0CH(第二个参数),才会跳转到CC17B1这个地址。
这里的eax是通过ebp+8获取到的,因为是从右往左压栈的,所以拿到的第一个参数是y。
分析反汇编并还原C语言代码:
首先push了5和4,因为是从右往左进行入栈的,所以在C中,第一个参数一定是4,第二个参数是5。
紧接着分析如下汇编指令:
首先将全局变量的地址(004225c4)存储到eax寄存器中,然后将eax赋值给局部变量,然后第一个参数(5),放到ecx寄存器中,然后第一个参数(5)和第二个参数(4)进行比较,如果5大于4的话则跳转到00401064这个地址,否则将4存储到edx寄存器中,紧接着拿出局部变量的地址所对应的值,然后和4相加,最后将edx寄存器中的值存储到全局变量的地址中(004225c4)。
mov eax,[004225c4]
mov dword ptr [ebp-4],eax
mov ecx,dword ptr [ebp+8]
cmp ecx,dword ptr [ebp+0Ch]
jg 00401064
mov edx,dword ptr [ebp+0Ch]
add edx,dword ptr [ebp-4]
mov dword ptr [004225c4],edx
int g_n = 10;
void function(int x,y int y){
int g = g_n;
if(x <= y){
g_n = g + y;
}
}
IF语句的反汇编判断:
IF_BEGIN:
先执行各类影响标志位的指令
jxx ELSE_BEGIN
....
IF_END:
jmp END
ELSE_BEGIN:
...
ELSE_END
END:
1.如果不跳转,那么会执行到jmp处,jmp直接转到到END处。
2.如果跳转,则会直接跳过jmp END处的代码,直接执行后面的代码。
总结:
跳转到一部分代码,不跳转执行另一部分的代码。
第一个jxx跳转的地址前面有一个jmp,可以判断是if....else语句。
例子:
分析如下反汇编代码:
int g_n = 10;
void function(int x, int y,int z) {
if (x > y) {
g_n = y;
}
else if (x < y) {
g_n = x;
}
else {
g_n = z;
}
}
int main()
{
function(4,5,6);
return 0;
}
首先cmp判断eax(x的值)的值如果小于或等于y的值,那么就跳转到51853这个地址,我们发现这个地址上面有一个jmp语句,那么就可以判断它是一个if....else了。
这里假设第一个参数是8,第二个参数是7。(function(8,7))
因为是从左到右入栈,所以第一个压入到栈的是7(ebp+8),第二个是8。(ebp+0Ch)
首先判断(ebp+8) 7是否小于等于8,如果条件成立的话那么直接跳转到004010f0这个地址。
紧接着判断判断(ebp+8) 7是否小于等于8,如果条件成立的话那么就跳转到401123这个地址。
否则
void function(int x,int y){
if(x > y){
int a;
int b = a+1;
}else if(x > y){
}
}
表达式
什么是表达式?
例如:这里的x+y+1就是表达式,但是没有用其他的值来接收,所以在反汇编中是看不到相应的汇编代码的。
void function(int x,int y) {
x + y + 1;
}
int main()
{
function(1,2);
}
特点一
表达式无论有多么复杂,都只有一个结果。
特点二
只有表达式是可以编译通过的,但是不会生成汇编代码,需要与赋值或者其他流程控制的语句一起使用。
特点三
当表达式中存在不同宽度的变量时,结果将转换为宽度最大的那个。
char a;
int b;
a = 10;
b = 20;
printf("%dn",a+b);
反汇编:可以看到首先将0A赋值给ebp-4,然后将14h赋值给ebp-8,在加之前将a转换成了4个字节,放到了eax这个寄存器中,最后再去和b相加。
逻辑运算符
如下C代码:我们想看的是与在汇编中是怎么样的。
void Fun(int x,int y,int z){
if(x > 1 && y > 1 && z >1){
printf("OK");
}else{
printf("Error");
}
}
int main(){
Fun(1,2,3);
}
反汇编:首先将第一个参数和1进行比较,如果第一个参数小于或者等于1的话就跳转到013017F2这个地址,然后接着比较第二个参数如果小于1或等于1的话就跳转到013017F2地址,第二个参数比较完成之后再比较第三个,如果小于1或等于1的话就跳转到013017F2地址。
如下C代码:我们想看的是与在汇编中是怎么样的。
void Fun(int x,int y,int z){
if(x > 1 || y > 1 || z >1){
printf("OK");
}else{
printf("Error");
}
}
int main(){
Fun(1,2,3);
}
反汇编:首先将第一个参数和1比较,如果第一个参数比1大的话那么就跳转到010F17E3这个地址,只要一个成立就将OK打印出来。
函数返回值是如何传递的
char类型的返回值
如下C代码:
#include <iostream>
char fun() {
//1000行代码
return 12; //值存储到AL
}
int main() {
char c = fun();
printf("%dn",c);
return 0;
}
可以看到在反汇编中它将0Ch存储到了AL这个8位寄存器中,0Ch转换成10进制就是12。
紧接着会将AL中的值存储到局部变量c中。
short类型的返回值
我们将返回值类型改为short之后会发现在汇编中,在返回char类型的时候是存储在AL寄存器中的,使用short之后发现是使用eax寄存器。
但是到最后存储的时候会发现使用的是ax寄存器给C这个局部变量赋值的
int类型的返回值
int类型是将值存储到eax寄存器中,然后通过dword取出来。
总结:
8位放到AL里面,16位放到AX里面,32位放到EAX里面。
练习
返回值超过32位时,存在哪里?用long long(int64)类型做实验,long long类型在VC6中对应的就是int64
__int64 Function(){
__int64 x = 0x123456789;
return x;
}
参数传递的本质
如下C代码:
void Function(char x,char y,char z) {
}
int main() {
Function(1,2,3);
return 0;
}
虽然我们传递的是char类型的,1个字节,但是实际上传递的是4个字节,反汇编如下:
可以看到ESP每次减了4。
那么short类型,或者int类型都是一样的。
那么为什么会这样呢?
本机尺寸,如果本机是32位的,那么对32位的数据处理是最好的。
编译器遵守了这个原则。
结论:整数类型的参数,一律使用int类型。
参数传递的本质就是将上层函数的变量,或者表达式的值复制一份,传递给下层函数。
局部变量的本质
我们在空函数反汇编中可以看到,当我们什么都不定义的时候,他会默认给我们分配40h的缓冲区空间,这里需要注意的是每一个编译器都是不一样的。
例如我们在VS中编译的话发现分配了C0h的缓冲区空间。
那么当我们定义一个局部变量的时候,这个空间就会提C个字节。(需要注意的是每一个编译器都是不一样的。
当定义两个局部变量的时候我们会发现又加了C个字节。
这里我们会发现无论类型是int还是short或char,都是提C个字节。
赋值语句的本质
赋值的本质就是将某个值存储到变量中的过程就是赋值。
数组的本质
上面定义的方式和下面定义的方式是没有任何本质区别的。
我们可以在反汇编中看到数组的本质其实就是上面定义的那些变量的反汇编。
数组其实本质就是一堆等宽的变量放到一起。
数组越界
void Function() {
int arr[5] = { 1,2,3,4,5};
/* arr[6] = 0x12345678;*/
printf("%dn", arr[6]);
}
int main()
{
Function();
}
可以看到如下图,数组是可以越界的,但是访问到的不是数组中给定的元素了,它访问到的是堆栈中的数据。
多维数组
二维数组初始化
int arr[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,7,6,5}
}
二维数组和一维数组比较:
内存大小比较:
#include <iostream>
void Function() {
/* int arr[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};*/
int arr[12] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
}
int main()
{
Function();
}
二维数组:
可以看到在反汇编之后是没有任何区别的。
void Function() {
int arr[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
/* int arr[12] = { 1,2,3,4,5,6,7,8,9,10,11,12 };*/
}
int main()
{
Function();
}
那么编译器是如何处理二维数组的呢?
如果碰见了二维数组编译器会这样处理:
int [3 * 4] = {};
二维数组编译器如何分配空间?
例如如下C代码:
int arr[4][12] = {
{1,2,3,4,5,6,7,8,9,10,11,12}, //第一年
{11,22,3,4,5,6,7,89,0,10,1,23}, //第二年
{3,231,2,231,1,12,3,1,31,89,75,12,67}, //第三年
{6,5,4,3,1,12,14,15,16,18,21,125} //第四年
}
比如说我们想获取arr[0][8],这里获取到的就是8这个数据。
那么编译器如何查询的呢?
arr[0 * 12 + 8] = arr[8]
那么如果想获取到第二年的下标为11的数据。
arr[1 * 12 + 11] = arr[23]
int arr[5][10] = {
{19,13,25,15,12,11,26,17,29,10},
{19,13,25,15,12,11,26,17,29,10},
{19,13,25,15,12,11,26,17,29,10},
{19,13,25,15,12,11,26,17,29,10},
{19,13,25,15,12,11,26,17,29,10}
};
arr[1][5]
编译器获取:
1 * 10 + 5 = 15 = arr[15]
结构体
结构体其实就是一个大杂烩,比如说我们想存储游戏里面的相关信息,比如说生命值,性别,血量,这一类数据的话,我们需要将它存储到一个容器里面,所以就可以使用结构体。
结构体定义
使用struct关键字来定义结构体,这里的st其实是和int short double这些类型是平级的。
那么也就是说st == int。
//告诉编译器 我现在有一个新的类型 下面那样的类型
struct st {
//可以定义多种类型
int a;
char b;
short c;
};
那么在编译期间是不会给他分内存的,只有在它使用的时候才会分。
比如说 st x;这样使用的时候才会分内存。
//告诉编译器 我现在有一个新的类型 下面那样的类型
//全局变量
st x;
struct st {
//可以定义多种类型
int a;
char b;
short c;
};
结构体的使用
#include <iostream>
struct AA {
int x;
};
//告诉编译器 我现在有一个新的类型 下面那样的类型
//int == st
struct st {
//可以定义多种类型
int a;
char b;
short c;
int arr[10];
AA aa;
};
st x;
void Function() {
x.a = 10;
x.b = 20;
x.c = 30;
//给数组赋值
x.arr[0] = 1;
//包含结构体赋值
x.aa.x = 10;
}
void Function2() {
int a1 = x.a;
int b2 = x.b;
int c3 = x.c;
int d4 = x.arr[0];
//可变参数
printf("%d %d %d %d", a1, b2, c3,d4);
}
int main()
{
//给全局变量赋值
Function();
//读取全局变量的值
Function2();
}
结构体的反汇编
全局变量反汇编:
struct st {
//可以定义多种类型
int a;
char b;
short c;
int arr[10];
AA aa;
};
st x;
void Function() {
x.a = 10;
x.b = 20;
x.c = 30;
//给数组赋值
x.arr[0] = 1;
//包含结构体赋值
x.aa.x = 10;
可以看到是对固定地址进行赋值的,因为在编译阶段地址就已经固定了。
局部变量的反汇编
struct AA {
int x;
};
//告诉编译器 我现在有一个新的类型 下面那样的类型
//int == st
struct st {
//可以定义多种类型
int a;
char b;
short c;
int arr[10];
AA aa;
};
void Function() {
st x;
x.a = 10;
x.b = 20;
x.c = 30;
//给数组赋值
x.arr[0] = 1;
//包含结构体赋值
x.aa.x = 10;
}
结构体如何给函数传值
如下C代码:
struct st {
//可以定义多种类型
int a;
int b;
int c;
int d;
int e;
int f;
};
st Function(st st1) {
st s;
s.a = 10;
s.b = 20;
s.c = 30;
s.d = 40;
s.e = 50;
s.f = 60;
return s;
}
int main()
{
st t;
t.a = 10;
t.b = 20;
t.c = 30;
Function(t);
}
反汇编:
这里首先将A 14 1E分别移动到ebp-1c 19 14的位置,然后将esp进行提栈,24个字节,18是16进制的,转换成10进制就是24,然后将esp的值给了eax,紧接着通过ebp-1c拿到的A这个值移动到ecx寄存器,然后将ecx寄存器中的值放到eax中,紧接着一个每次放一个值然后eax + 4的位置。
堆栈图:
结构体的返回值
反汇编:
Sizeiof的使用
printf("%dn", sizeof(char)); //1
printf("%dn", sizeof(short)); //2
printf("%dn", sizeof(int)); //4
printf("%dn", sizeof(long int)); //4
printf("%dn", sizeof(__int64)); //8
printf("%dn", sizeof(float)); //4
printf("%dn", sizeof(double)); //8
char arr1[10] = { 0 };
int arr2[10] = { 0 };
short arr3[10] = {0};
printf("%dn", sizeof(arr1)); //10
printf("%dn", sizeof(arr1)); //40
printf("%dn", sizeof(arr1)); //20
结构大小:
我们会发现按道理说应该S1和S2都是6个字节的,但是为什么他们除了顺序不一样之外其他都是一样的,打印出来的字节却不是一样的。
结构体对齐
结构体对齐方式:
#pragma pack(1)
结构体
#pragma pack(1)
这里的pragma pack(1)表示使用1字节进行对齐。
就比如说如下C代码:
#pragma pack(1)
struct s {
char a;
int b;
short c;
};
#pragma pack(1)
那么一字节对齐的话内存就变成这样了。
那么如果是两字节对齐的话:
struct s {
int a;
__int64 b;
char c;
};
4个字节对齐:
struct s {
int a;
__int64 b;
char c;
};
8字节对齐:
struct s {
int a;
__int64 b;
char c;
};
字节对齐原则:
1.数据成员对齐规则: 结构体的数据成员,第一个数据成员放在offset为0的地方,以后每个数据存储的起始位置要从该成员的大小的整数倍开始。
2.接受体的总大小,也就是sizeof的结果,必须是其内部最大成员的整倍数,不足的要补齐。
例如:
如下对齐方式为8.(比如如果这个结构体中最大的值是4的时候,那么最大的整数倍就不是8了就是4了。)
struct s1 {
char a;
int b;
char c;
};
首先char类型占用一个字节
a
紧接下一个类型是int类型,那么就以int类型和8进行比较,int类型4个字节,比8小,所以以int类型为准,所以a后面需要补3个0,然后放入int类型。
a 0 0 0
b b b b
紧接下一个使用char类型和8进行比较,char类型小于8,所以以char类型为主。但是我们最终要以4的倍数,因为这个结构体中4是最大的。所以最终就变成了12。
a 0 0 0
b b b b
c 0 0 0
例子2:
struct s1 {
int a;
char b;
char c;
};
首先int类型占用4个字节,然后char类型和8进行比较,那么肯定是char类型低于8的,所以以char类型为准,那么紧接着还是char类型所以还是以char类型为准,最后必须是4的整倍数,因为这个结构体中最大的是int类型,所以最后的结果是8。
a a a a
b b b b
建议:
按照数据类型由小到大的顺序进行书写。
Typedef关键字
为现有的类型定义别名:
typeofdef unsigned char BYTE;
typeofdef unsigned short WORD
typeofdef unsigned int DWORD
字符数组赋值
#include <iostream>
#pragma pack(8)
typedef struct student {
int x;
char y;
char arr[10];
}stu;
int main()
{
student s;
s.x = 10;
s.y = 20;
s.arr[0] = 'c';
s.arr[1] = 'h';
s.arr[2] = 'i';
s.arr[3] = 'n';
s.arr[4] = 'a';
s.arr[5] = ''; //这里的0表示截取的意思,只要见到0后面无论还有其他什么都不管
printf("%d %dn %s", s.x, s.y,s.arr);
}
#define _CRT_SECURE_NO_WARNINGS //使用strcpy,strcat等函数安全性过低,会报警告 所以需要加上这个
#include <iostream>
#include <string.h>
#pragma pack(8)
typedef struct student {
int x;
char y;
char arr[10];
}stu;
int main()
{
student s;
s.x = 10;
s.y = 20;
/*s.arr[0] = 'c';
s.arr[1] = 'h';
s.arr[2] = 'i';
s.arr[3] = 'n';
s.arr[4] = 'a';
s.arr[5] = '';*/
strcpy(s.arr, "china");
printf("%d %dn %s", s.x, s.y,s.arr);
}
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论