逆向从基础到大仙,全方位系统性学习软件逆向安全 不要问为什么收费,逆向课程外面4000起 这点钱400-700课,很合适,课程还在更新,并未完结!

OllyDbg介绍

Ollydbg 简称 OD,是一个动态调试器 是反汇编工作的常用工具,也是咱们的汇编学习环境
下载OllyDbg|https://down.52pojie.cn/Tools/Debuggers/吾爱破解专用版Ollydbg.rar

OllyDbg界面

打开之后应该是这样子的 Ps:与吾爱破解没有关系,单纯借用od,原版OllyDbg也可以,看雪也有OllyDbg 先来界面介绍:
-反汇编窗口:显示被调试程序的反汇编代码 -寄存器窗口:显示当前所选线程的CPU寄存器内容 -信息窗口:显示反汇编窗口中当前选中的第一条命令的参数及一些跳转目标地址、字串等(一般动态加密解密字串等会在此出现)。 -数据窗口:显示内存或文件的内容。 -堆栈窗口:显示当前线程的堆栈数据。比如程序初始化的一些常量、变量之类的数据。 -菜单窗口:一些常用调试命令以及操作。一般把鼠标移到菜单窗口的某按钮后,界面最左下角会出现一些命令提示,比如单步步入(F7),单步步过F8,运行程序(F9)等。 -命令行窗口:用来下断点等相关命令。

OllyDbg配置

一般配置一个界面目录即可,右键打开方式可以添加可以不添加 如果不去配置目录路径,可能造成程序无法使用

复习巩固

0、进制

这一节课,我们学习了进制,现在来巩固一下重要的知识点。 1、计算机只认识二进制,所有数据都以二进制存储到机器中 2、二进制逢二进一,八进制逢八进一,N进制逢N进一,下标于0开始 3、了解进制进位的方法
0|1 ??|??
上面是二进制,那么两个??应该写什么,轻以??/??的格式写出答案,注意符号英文
请输入你思考后的答案|二进制进位|zhishihezi.net|10/11
再来复习一下进位表

1、数据宽度

在这节课中,我们知道了数据存储是有宽度的,否则超出的数据就会被抛弃。 常见单位如下:
-位 Bit 只存一位二进制 -字节 Byte 可存储八位二进制0~FF -字 Word 可存储十六位二进制0~FFFF -双字 Dword 可存储三十二位二进制0~FFFFFFFF
如图所示,已经超出了存储的范围,请问,会抛弃哪个数据?
请问会抛弃哪个数据?|!AF|2B|都不抛弃
如果回答错了,回去复习一下章节内容哦。
在编程中,字母表示的进制
|二进制=0b |十六进制=0x |八进制=0

2、有无符号数

无符号数比有符号数存储的要多要大。所谓有符号数也就是有负数。无符号数全部都是正数 如:0x69AB3037 这个是一个有符号数,那么请问,他的符号位是正数还是负数
请作答|!正数|负数
至于为什么是你选择的正确答案,还记得要转换成几进制去看符号位吗?

2、原码反码与补码

原码:

正数:符号位不变,其余位取绝对值本身 负数:符号位不变,其余位取绝对值本身

反码:

负数:符号位不变,其余位取反

补码:

负数:反码+1 如图所示,还记得将一个负数存储到内存中,是以什么方式存储吗?
请作答|原码|反码|!补码
好的,这里就当你一次作答成功,很棒哦~
超级训练|请写出-3在内存中存储的十六进制|zhishihezi.net|FD

3、位运算

我们学习了如下几个位运算:
-与运算(汇编表示:and | C++:&):两个都为1就为1 -或运算(汇编表示:or | C++:|):有一个为1就为1 -异或运算(汇编表示:xor | C++:^):不一样的时候为1 -非运算(汇编表示:not | C++:~):取反 -左移(汇编表示:shl | C++:<<):左移n位,补0 -右移(汇编表示:shr/sar | C++:>>):右移n位,补0,有符号补符号位

新大陆-寻址大陆

所谓寻址方式,也就是在OD中寻找地址的方式 综合性来讲,一共分为两种:间接寻址方式

直接寻址方式

就像这种,地址简单易懂,直接出现在指令之中,叫做直接寻址方式 mov
,reg mov reg,
这里的reg为任何一个通用寄存器

间接寻址方式

这个样子,地址需要通过计算才能知道的,属于间接寻址方式,无法立马看出地址的。 mov eax,
寄存器间接寻址 mov eax,
mov eax,
乘的立即数是{1、2、4、8}之间的其中一个,为其他数值会报错 mov eax,
以上reg为寄存器,立即数为整数数字

MOV指令的用法

MOV 数据传送指令 将一个数据从源地址传送到目标地址 前提:数据长度要对等 mov 目标地址,源地址 执行完mov指令后,目标地址里的数据会被源地址的数据替换 至于其他用法,我们可以自己挖掘,也可以以后慢慢来 即使不想去挖掘,也要知道,mov是个数据传送指令 就好像mov eax,1 就是把1给eax 这节课以后要确保在OllyDbg里看到mov指令后能够明白是什么意思!

答题测试


数据是什么方式存放的|大端存储模式|!小端存储模式

常用汇编指令(一)

1、MOV 数据传送指令

注:r = 通用寄存器,m = 内存,imm = 立即数,r8 = 8位通用寄存器,m8/imm8同理

Opcode
|
Instruction
|
Op/En
|
64-Bit Mode
|
Compat/Leg Mode
|
Description
88 /r|MOV r/m8,r8|MR|Valid|Valid|Move r8 to r/m8. 88 /r|MOV r/m16,r16|MR|Valid|Valid|Move r16 to r/m16. 88 /r|MOV r/m32,r32|MR|Valid|Valid|Move r32 to r/m32. 8A /r|MOV r8,r/m8|RM|Valid|Valid|Move r/m8 to r8. 8B /r|MOV r16,r/m16|RM|Valid|Valid|Move r/m16 to r16. 8B /r|MOV r32,r/m32|RM|Valid|Valid|Move r/m32 to r32. B0+ rb ib|MOV r8, imm8|OI|Valid|Valid|Move imm8 to r8. B8+ rw iw|MOV r16, imm16|OI|Valid|Valid|Move imm16 to r16. B8+ rd id|MOV r32, imm32|OI|Valid|Valid|Move imm32 to r32.
用法如下(没列举的请自行动手实验): 上面这些是我从英特尔白皮书里copy出来的常用用法 完整白皮书参见:
Intel开发人员手册|https://software.intel.com/content/dam/develop/public/us/en/documents/325462-sdm-vol-1-2abcd-3abcd.pdf


Intel开发人员手册|https://software.intel.com/content/www/us/en/develop/download/intel-64-and-ia-32-architectures-sdm-combined-volumes-1-2a-2b-2c-2d-3a-3b-3c-3d-and-4.html

2、ADD 加法指令(结果返回于EAX)

注:r = 通用寄存器,m = 内存,imm = 立即数,r8 = 8位通用寄存器,m8/imm8同理

Opcode
|
Instruction
|
Op/En
|
64-Bit Mode
|
Compat/Leg Mode
|
Description
80 /0 ib|ADD r/m8, imm8|MI|Valid|Valid|Add imm8 to r/m8. 00 /r|ADD r/m8, r8|MR|Valid|Valid|Add r8 to r/m8. 02 /r|ADD r8, r/m8|MR|Valid|Valid|Add r/m8 to r8.
用法如下(32和16位自行动手): Ps:一个个复制粘贴太累了,16和32同理记得就行了,不行下个白皮书对照着去做

3、SUB 减法指令

注:r = 通用寄存器,m = 内存,imm = 立即数,r8 = 8位通用寄存器,m8/imm8同理

Opcode
|
Instruction
|
Op/En
|
64-Bit Mode
|
Compat/Leg Mode
|
Description
80 /5 ib|SUB r/m8, imm8|MI|Valid|Valid|Subtract imm8 from r/m8. 28 /r|SUB r/m8, r8|MR|Valid|Valid|Subtract r8 from r/m8. 2A /r|SUB r8, r/m8|MR|Valid|Valid|Subtract r/m8 from r8.
用法如下(其他例子不再写了,一通百通,32和16位自行动手):

4、AND 与运算

注:r = 通用寄存器,m = 内存,imm = 立即数,r8 = 8位通用寄存器,m8/imm8同理,太长了,去掉一些好了

Instruction
|
Description
AND r/m8, imm8|r/m8 AND imm8. AND r/m8, r8|r/m8 AND r8.
用法如下(32和16位自行动手):

5、OR 或运算

注:r = 通用寄存器,m = 内存,imm = 立即数,r8 = 8位通用寄存器,m8/imm8同理,太长了,去掉一些好了

Instruction
|
Description
OR r/m8, imm8|r/m8 OR imm8. OR r/m8, r8|r/m8 OR r8.
用法如下(32和16位自行动手):

6、XOR 异或运算

注:r = 通用寄存器,m = 内存,imm = 立即数,r8 = 8位通用寄存器,m8/imm8同理,太长了,去掉一些好了

Instruction
|
Description
XOR r/m8, imm8|r/m8 XOR imm8. XOR r/m8, r8|r/m8 XOR r8.
用法如下(32和16位自行动手):

7、NOT 非运算

注:r = 通用寄存器,m = 内存,imm = 立即数,r8 = 8位通用寄存器,m8/imm8同理,太长了,去掉一些好了

Instruction
|
Description
NOT r/m8|Reverse each bit of r/m8.
用法如下(32和8位自行动手):

常用汇编指令(二)

1、MOVS指令:移动数据 (内存 --> 内存)

可移动三种空间:Byte、Word、Dword MOVS Byte
,Byte
MOVSB MOVS Word
,Word
MOVSW MOVS Dword
,Word
MOVSD 默认使用EDI和ESI ESI:内存地址,要复制的数据存储地址 EDI:内存地址,要把数据复制到的存储地址 在OllyDbg中输入MOVSB、MOVSW、MOVSD,自动填充指令,
注意,移动的是地址指向的内存数据,而非地址
DF标志位(方向标志位):DF标志位为0时执行一次加1/2/4,为1时执行一次则减1/2/4,根据Byte/Word/Dword决定 实战:

执行前:

执行后:


DF标志位为1的时候,会发生什么?请自己动手实验

2、STOS指令:将eax中的数据放入的edi所指的地址中(地址
指向的内存
),同时,edi会增加?个字节

三种大小:Byte、Word、Dword STOS Byte
---> 简写STOSB STOS Word
---> 简写STOSW STOS Dword
---> 简写STOSD DF标志位(方向标志位):DF标志位为0时执行一次加1/2/4,为1时执行一次则减1/2/4,根据Byte/Word/Dword决定 实战:

执行前:

执行后:


DF标志位为1的时候,会发生什么?请自己动手实验

3、REP指令:按计数器(ECX)中的次数重复执行指令

如: MOV ECX,0x5 REP MOVSD REP STOSD 实战:

执行前(标注为执行后的推算):

执行后:

执行完后和所想一样 ECX由5变成0 ESI由19FF88 + 4 + 4 + 4 + 4 + 4 = 19FF9C EDI由19FFB0 + 4 + 4 + 4 + 4 + 4 = 19FFC4 因为DF标志位为0,并且是MOVSD(Dword四字节),所以ESI / EDI都是+ 4,而不是-4,也不是+1 / +2 19FF88:00200000 (HEX) 19FF8C:8477B7DC (HEX) 19FF90:00000000 (HEX) 19FF94:00000000 (HEX) 19FF98:00200000 (HEX) 以上五个地址指向的内存数据,也就是ESI赋值替换到EDI的地址指向的数据 19FFB0:00000000 (HEX) ----> 00200000 (HEX) 19FFB4:00000000 (HEX) ----> 8477B7DC (HEX) 19FFB8:00000000 (HEX) ----> 00000000 (HEX) 19FFBC:00000000 (HEX) ----> 00000000 (HEX) 19FFC0:00000000 (HEX) ----> 00200000 (HEX)
最后结论
rep就是根据ecx的十六进制数转成十进制后(执行次数),重复执行这条指令 MOVS / STOS 都是根据方向表示为DF标志位决定加减,根绝 Byte / Word / Dword 决定加减多少

课后题:

请根据上面rep movsd指令去思考rep stosd指令后的改变 熟悉DF方向标志位为0和1的时候的区别 熟悉本章指令,熟悉到以后在od中看到不需要去查阅资料,就可以知道此汇编语句的意思即可

什么是堆栈?

堆栈:一块特殊的内存,软件启动的时候,是系统分配给程序使用的,存储临时数据等 和普通内存不一样的地方在于,一个是人工申请,一个是系统分配。 堆栈可以称为是一个程序的核心,所有的数据都会在堆栈中展现显示出来。 堆栈的数据是由下往上使用的。 由下向上使用

如何查看系统为我们分配的堆栈大小?

FS后面有一个地址,我们通过下面的命令窗口 dd 地址,以dword查看,现实的线程堆栈底和顶部就是堆栈的开始和结束 如果堆栈被用完,超出大小,就会报错,产生堆栈溢出 可以把鼠标从堆栈窗口拖动滚动条,查看最顶部和最底部地址,底部地址可能不同,+4的话就可以对起来,因为那一块也要被使用

如何查看堆栈使用到哪里了?

我们还记得八个寄存器的作用吗? EAX:存储返回值 ECX:计数器,循环之类的指令就看ECX
ESP:栈指针寄存器,栈顶寄存器,当前正在使用的堆栈地址,永远指向堆栈顶部
如果堆栈调乱了,就可以跟随esp回到使用的堆栈地址

程序如何使用堆栈?

我们想要为0x19FF70地址存储数据,可以通过mov指令存储,但是为什么有个esp-4呢? 1、mov dword
,11223344 将数据存储到了0x19FF70 2、执行完毕,ESP不变,数据存储完成,因为ESP指向的还是源地址,所以如果有下一条存储指令,就会覆盖存储的那个数据 3、所以要sub esp,0x4,让esp-4,指向我们赋值的地址 4、如果不想要我们刚才要的数据了,那么再去add esp,0x4,再让esp+回去即可 至于+4+8+C,加多少,取决于你往堆栈写入了多少数据,要平衡堆栈,这上面一堆操作堆栈的步骤有点繁琐? 那么,它来了,虽然有它,但是我们要知道,它做了什么事,上面的指令,就是它执行的原理 -------------------------------------------------------------------------------------------------------- PUSH:压栈,将数据压入堆栈,并且修改esp指向 PUSH 寄存器 PUSH 立即数 PUSH 内存 以上是他的所有可以用的用法,下面实战: push eax 将eax压入堆栈,我们执行。 eax被压入堆栈,并且esp发生改变。 -------------------------------------------------------------------------------------------------------- 那么压入堆栈了,我想把数据取出来怎么办? 现在eax的值已经被压入堆栈,我想重新取回到eax里,那么就 mov eax,dword
把esp地址指向的数据,也就是刚刚压入堆栈的数据,传递给eax 然后add esp,4,数据弹回远处,那么堆栈也要恢复到原来操作之前的状态,不然会崩,所以add esp,4,进行恢复 可以看到,执行完后,eax重新取回,堆栈恢复原样,那么有没有像PUSH一样,优雅且简单的指令替代他们两条呢? 答案是:有 -------------------------------------------------------------------------------------------------------- POP:出栈,将数据弹出堆栈,并且修改esp POP 寄存器 POP 内存 以上是他的所有可以用的用法,下面实战: EBP的值被push压入堆栈,那么我们再取回去 因为我为了方便查看,把堆栈中栈顶数据改为99999999了 pop执行过后,栈顶数据99999999被弹回ebp esp+4,回到push之前的状态。

课后

1、请自己动手完成push和pop指令的练习 2、尝试翻译简单的汇编代码,也就是od里的代码,按键盘的
;
键可为汇编语句添加注释,给能看懂的汇编语句添加上自己理解的意思,如果不确定对错,可加我二维码。 3、摸清楚push和pop指令的原理。

如何修改EIP?

修改通用寄存器可以用mov指令 但是无法修改eip 因为,
EIP存放的是CPU下一次要执行的指令的地址
, 但是有三个指令,可以修改掉他,分别是JMP、CALL、RET 也就是,修改了CPU下一次执行的指令地址。

JMP指令

无条件跳转。修改EIP,且只影响EIP。内存/立即数/寄存器,都可以操作 修改前: 修改后:

CALL指令

如果要执行CALL指令,
要用F7
,不要平时的执行方法,用F8去执行 会影响栈顶地址 / ESP / EIP,因为CALL因为要记录一个返回的地址(也就是CALL的下一行) 修改前: 修改后:

RET指令

既然有CALL,那么就一定有RET。 简意:通俗讲也就是把CALL压入堆栈的值,再返回给EIP,然后栈顶+4。 修改前: 修改后:

CALL与RET的执行原理

CALL:

CALL指令修改了EIP 向堆栈存储了一个值(CALL指令下一行的地址) ESP的值发生了改变 那么,用其他指令写法就如下:
//怎么修改ESP和堆栈? //因为CALL下一行的地址要作为返回地址入栈,所以我们来手动push入栈 push CALL下一行指令的地址 //怎么修改EIP? //然后让EIP指向CALL函数内的第一行 jmp CALL函数地址

RET

修改了ESP,因为返回地址后,esp+4,平衡了堆栈 修改了EIP,eip又指向了call函数的下一行的地址 代码如下:
//ESP+4,堆栈平衡 ADD ESP,4 //改变EIP,eip指向下一次要执行指令的地址 jmp

关于CALL的F7与F8

单步步入(F7),单步步过(F8) 在平时的指令上F7 / F8你可能看不出哪有不同,但是在CALL上的时候 按F8就会直接运行到CALL指令的下一行,把CALL当一行指令来执行,F7就会进入CALL,执行CALL内的指令(自己动手实践发现差别)

什么是函数?

函数就是一系列指令的集合,为了重复使用方便。 比如,我们要重复使用一堆指令如下:
00418840 mov eax,1 00418841 mov edx,2 00418842 add eax,edx
我们要重复使用这个加法,那么应该怎么去做?

函数怎么调用?

CALL / JMP 代码如下:
00418840 mov eax,1 00418841 mov edx,2 00418842 add eax,edx 00418843 jmp 函数的下一行地址(因为要返回去继续执行指令)
我们可以JMP 00418840,就开始了函数执行 最后再JMP 0041884X,跳回函数执行下一行。 但是如果我们想要再用呢?这个函数的最后一行还是jmp那个固定地址(41884X),无法返回到正确的地址,就会出错。 那么CALL指令呢? 还是如下代码:
00418840 mov eax,1 00418841 mov edx,2 00418842 add eax,edx 00418843 retn
函数调用: CALL 00418840 CALL执行后,就会将返回地址压入堆栈,然后RET返回。 重复调用,也不会像JMP一样,返回到一个错误不正确的地址。

怎么为函数传递参数?


00418840 mov eax,1 00418841 mov edx,2 00418842 add eax,edx 00418843 retn
那么我们不想让eax和edx固定为1和2,我们想执行的时候传递不一样的数值,怎么做? 我们就可以用到参数,那么比如现在我们的函数如下。
00418842 add eax,edx 00418843 retn
那么,我们在调用函数之前,就要把我们需要的数值给弄好,所以,在CALL调用函数之前,就要进行赋值
00418812 mov eax,5 00418813 mov edx,6 00418814 call 00418842
这样,我们就完成了参数的赋值,call函数开始之前,用mov指令将eax改为5,edx改为6,然后去执行函数。 这样的方法,就可以让我们每次调用函数都可以传递不同的参数和值。

参数有很多,怎么办?

刚才我们学习了利用寄存器传递参数,但是我们的参数超过了寄存器可存储的极限(一共八个寄存器) 而且还要排除掉一些修改后可能会报错的寄存器,那么不够用,怎么办?重点来了:
堆栈传参

//修改堆栈用push 0045D735 push 0x1 0045D737 push 0x3 0045D739 push 0x5 0045D73B call 0045D752

//函数执行代码,为什么要esp+C赋值给eax,因为CALL进入函数执行时,esp的栈顶第一行是CALL指令的下一行地址 //根据图示,双击堆栈的栈顶地址,进入偏移模式,第一个参数1是0xc,第二个是0x8,第三个参数是0x4 0045D752 mov eax,dword ptr ss:
//第一个参数给eax 0045D756 add eax,dword ptr ss:
//第二个参数与eax相加,放到eax 0045D75A add eax,dword ptr ss:
//第三个参数和第二个参数与eax相加过后的数值相加,放到eax 0045D75E retn //返回
这样,我们就完成了一个多参数的传递和函数调用

堆栈平衡!


什么是堆栈平衡?
进行堆栈操作的时候,在执行RET返回前,ESP指向的是我们对堆栈产生修改前的地址。

堆栈平衡的第一种情况。

如下:进行修改堆栈前,如果我们的函数执行的时候,对堆栈进行了修改操作,看下一行的push eax 此时我们的esp指向的还是ret要返回的call指令的下一行的地址 执行指令后如下: 栈顶已经指向了00000001的地址,那么ret就会返回到00000001的地址,而不是call指令的下一行地址 那么这个样子程序就会报错,所以我们要堆栈平衡,让堆栈原来是什么样子,就恢复成什么样子。

堆栈平衡的第二种情况。

那么就好像我们那个多参数传递加法,堆栈平衡吗?是不是没有堆栈操作指令? 答案是不平衡,因为我们call指令之前,进行了push,往堆栈传递了参数。 我们执行完了函数,返回到call下一行地址的时候,堆栈里仍然存储着无用的参数。 如下图: 操作后: 我们本来应该指向到操作前的 0019FF74 7673F989 但是操作后存储着垃圾参数,并未指向到原来的样子

那么如何平衡堆栈?

第一种:外平栈

如图所示,add esp,0xc 让esp回到了原来的指向。在函数外进行平栈的方法,叫做
外平栈

第二种:内平栈

在函数ret上,加上原来指向地址上面地址的偏移数值。我们这是c,就ret c 最后返回到了call指令的下一行地址,并且堆栈的指向恢复到了原样。 这样在函数内平衡堆栈的方法,叫做
内平栈
恢复到了最初的样子

什么是寻址方式

分为两种:ESP寻址与EBP寻址 先来回顾下我们之前的传参方式:

1、寄存器传参

2、堆栈传参

3、堆栈平衡

两种方式:内平栈和外平栈

ESP寻址

前面的两张图,无论是堆栈还是寄存器传参,都是操作ESP寻址。 但是弊端比较大。 就好像,如果现在eax,ecx,edx,ebx,esp,ebp,esi,edi 八个通用寄存器,全部存储着数值。 我们操作后,无法恢复原存储数值,那么后面的如果用到就可能会出错。

所以,应该怎么解决原数据丢失问题?

就要备份寄存器,怎么备份?把寄存器原数据压入堆栈。 那么如果我如图所示,这样操作对吗? 答案是不对,因为进入函数后,又进行push保存寄存器,所以用esp+c +8 +4无法取出正确参数 如图,参数位置变成了+14 +10 +c 所以导致我们用ESP寻址方式时,每次都要去算参数的位置。很麻烦。 毕竟esp指向的是栈顶,每次对堆栈操作就会发生改变,每次都要修正变化,否则就无法正确运行 所以ESP寻址,并不是最佳方式。 这里的pop,出栈指令,是和push相反的,先push的ecx,又push的edx 出栈时就先出edx,再出ecx

EBP寻址

如图所示,EBP寻址也就是暂时接管了ESP
0045D735 > 6A 01 push 0x1 //入栈 1 0045D737 6A 06 push 0x6 //入栈 6 0045D739 E8 17000000 call test.0045D755 //1和6参数入栈,call 0045D73E 90 nop

0045D755 55 push ebp //ebp入栈,先保存ebp原数据 0045D756 8BEC mov ebp,esp //把esp值给ebp 0045D758 83ec05 sub esp, 5 //esp - 5,减多少都可以 0045D75B 8B45 0C mov eax,dword ptr ss:
//ebp+c 参数1给eax 0045D75E 0345 08 add eax,dword ptr ss:
//参数6和eax相加,存于eax 0045D761 8BE5 mov esp,ebp //最后恢复esp 0045D763 5D pop ebp //恢复ebp原数据 0045D764 C3 retn 8 //返回call下一行地址
最后的ret8,也可以用外平栈,根据自己的喜好

代码执行 堆栈变化流程图:

可能会疑问第七步,我这里做一下解释 因为最后的ESP = 19FF68 但是我们还有一个ret的图我没画,因为有外平栈和内平栈两种方法 所以就普通ret的话,应该是ESP = 19FF6C,然后再去外平add esp,8 = 19FF74 或者内平栈ret 8,相当于返回并且add esp,8

JCC指令集合

以下所有JCC指令都受到标志位影响,先去看标志位章节,了解了标志位,根据影响标志位来看,就知道是怎么回事了


JCC指令|中文释义|影响标志位 JE/JZ|为0则跳转;相等则跳转|ZF=1 JNE/JNZ|不为0则跳转;不相等则跳转|ZF=0 JS|结果为负数跳转|SF=1 JNS|结果非负数跳转|SF=0 JP/JPE|结果若1出现次数为偶数则跳转|PF=1 JNP/JNO|结果若1出现次数为奇数则跳转|PF=0 JO|结果溢出跳转|OF=1 JNO|结果无溢出跳转|OF=0 JC/JB/JNAE|(无符号数)小于等于则跳转|CF=1 JNC/JNB/JAE|(无符号数)大于等于跳转|CF=0 JBE/JNA|(无符号数)小于等于跳转|CF=1/ZF=1 JNBE/JA|(无符号数)大于则跳转|CF=0/ZF=0 JL/JNGE|(有符号数)小于则跳转|SF≠OF JNL/JGE|(有符号数)大于等于跳转|SF=OF JLE/JNG|(有符号数)小于等于跳转|ZF=1/SF≠OF JNLE/JG|(有符号数)大于则跳转|ZF=0/SF=OF
引用一下别的地方的标志寄存器图: 现在中断和单步标志先不用了解

标志位介绍

1、CF 进位标志位,算术操作最高位结果进位(加法)或借位(减法) CF=1

EAX清零,CF=0,让FF+1,产生进位。 产生进位了,CF=1,借位自己动手sub

2、PF 奇偶标志位,结果中的低八位1的个数为偶数 PF=1

还是,al=FE,二进制也就是1111 1110 +1变成,1111 1111,这样就是偶数,PF=1,测试下。 和我们想的,一样,也就是低八位al,转成二进制,1的个数为偶数则为1

3、AF 辅助进位标志,算数操作在结果D3→D4位发生进位或借位时 AF=1

不常用,基本用不到,不介绍

4、ZF 零标志位:操作的结果等于0时 ZF=1

结果为0,置1 经常与CMP/TEST指令搭配使用
CMP
:类似
SUB
,但是不会保存结果到第一个操作数 赋值al和cl都为0xFE 然后cmp比较 ZF=1,说明结果为0,也相当于al - cl
TEST
:类似
AND
,但是结果并不保存 与运算:都为1就为1,所以不等于0,ZF=0

5、SF 符号标志位:结果最高位为1时 SF=1

清空SF和EAX 操作11 + EE = FF = 1111 1111 符号位为1,则SF=1

6、DF 方向标志位:1=递减,0=递增

MOVS、CMPS、SCAS、LODS以及STOS影响 也就是前面演示的,执行完指令后,是递增还是递减 STD设置DF为1、CLD设置DF为0

7、OF 溢出标志位:无符号运算是否溢出看CF,有符号运算是否溢出看OF

最大的有符号数为7F,那么如果7F+1,就会溢出。OF就会为1,我们试一下 清空al和OF 溢出,OF = 1

C语言环境介绍

IDE:Visual Studio C++ 6.0 / Visual Studio 2019 系统:Windows10 / 7 / XP 前言:因为后续的IDE添加的冗杂和帮助代码过多,推荐用Visual Studio C++6.0,添加代码少,可以让自己动手写,体积小,方便,速度快。
VC6+插件血慢下载地址|https://tymj.lanzoui.com/b07xxl37a

C语言环境配置

1、下载VC6.0完整绿色版 英文,管理员打开下一步安装即可,如果不会安装...... 2、如下图所示,打开文件Visual Studio C++ 6.0所在位置 3、如下图所示,默认会进入Bin目录,返回到上级MSDev98目录,然后选择AddIns目录 4、解压AddIns.7z压缩包,如图所示,双击VC6LineNumberAddin.reg合并注册表,或者右键选择合并即可(如果无效请找我) 5、打开VA_X_Setup1822.exe,进行安装,根据提示选择目录即可,一定要记住自己的安装目录。 我的安装到了C:\Program Files (x86)\Visual Assist X 6、解压VA_X1822.rar压缩包,把VA_X.dll替换进来即可 7、打开Visual Studio C++ 6.0,选择Tools - Customize 8、选择Add-ins and Macro Files选择夹 9、刚开始你们可能是空的,或者只有一个VC6LineNumber Developer Studio Add-in未选中 点击Browser...选择C:\Program Files (x86)\Visual Assist X\
VAssist.dll
(根据你们自己的安装目录选择) 然后再Browser...选择AddIns目录的VC6LineNumberAddin.dll即可 最后全部选中打上勾启用,重启VC++6.0即可

第一个C程序

1、

2、选择Win32 Console Application,并且填写项目名

3、选择An Empty Project,然后Finish - OK

4、选择FileView栏目,展开项目,选择Source Files

5、然后New 新建源文件

6、选择C++ Source FIle并且填写名字

7、一段简单的代码

先不用管下面的代码是什么意思,这个是入口程序,死的。 程序开始执行的时候,就是从入口程序开始执行。
void main() //程序入口,开始执行的地方 { return; //执行结束 }

8、现在我们去构建(F7),并且运行(F5)程序

我们会发现一个黑窗口一闪而过~ 那是因为我们现在什么都没有写,这程序是空的。 但是我们学过汇编,所以可以在花括号内插入 必须要用__asm开头,告诉编译器,我们这写的是汇编代码,如果直接写mov或者add等,编译器是无法识别的。
void main() //程序入口,开始执行的地方 { __asm { mov eax,eax mov ecx,ecx mov edx,edx } return; //执行结束 }
发现程序还是一闪而过了,因为我们插入的汇编代码也是无用代码,花指令,程序还是空的

9、了解程序的运行本质就去看他的汇编代码

利用断点(F9),让程序暂停在我们想暂停的地方,然后我们再去构建(F7),运行(F5) 断下来之后是这个样子的。 你们的应该和我不一样,可以通过View - Debug Windows,自己调节

10、在代码区右键,Go To Disassembly,查看汇编代码

我们的__asm上面和return下面的代码,都是编译器帮我们生成的函数 这里单步快捷键是F10。 我们F10到代码的最后一句ret,也就是程序结束的地方,自己体会下程序运行过程。 最后我们跑过ret指令后,Shift + F5结束调试。

11、体验C语言的函数

那么想一下,还记得汇编里的函数吗?
函数:一系列指令的集合,为了重复使用调用
在C语言中,他是如下格式调用
函数类型 函数名(参数) { 程序执行代码; }


重点

(1)函数类型、函数名,不可省略,参数可以省略(不必要/不重要的参数)

(2)函数名、参数名,只能
以字母、数字、下划线组成
,且
第一个字母必须是字母或者下划线

(3)区分大小写,不可使用关键字

例:
int jiafa(int a,int b) { return 0; }
那么,这样一个简单的函数就写完了!
如何去调用?
在汇编中我们都知道是如下格式:
push 1 //传参 push 2 //传参 call 内存地址 //CALL到函数
在C语言中调用是这个样子的:
jiafa(参数); //用我们的例子就是: jiafa(1,2); //这样就是把1和2传入
现在我们来实操一下。
int jiafa(int a,int b) //自定义的函数jiafa { return 0; } void main() //程序入口,开始执行的地方 { jiafa(5,5); //调用函数 return; //执行结束 }
可能这样,你还比较迷茫,不知道这些代码的意思。 别着急,发挥我们强大的汇编基础知识,F9在调用函数的地方下个断,然后大胆的去F7、F5 断下来之后,我们继续右键 Go To Disassembly。 我们看这里的汇编代码,我们写了一个函数,编译器为我们生成了一堆汇编代码。 但是我们应该有了汇编基础,看这些汇编代码。 我们应该可以对这个函数完全掌握了解。 然后我们单步步过(F10),单步步入(F11),单步执行和进CALL。 到了我们的函数区代码。 这些代码,应该我们都能看明白了,后面我们慢慢的去画图来了解程序的每一次的执行。 记住函数的代码
// 1: int jiafa(int a,int b) // 2: { 00401010 push ebp 00401011 mov ebp,esp 00401013 sub esp,40h 00401016 push ebx 00401017 push esi 00401018 push edi 00401019 lea edi,
0040101C mov ecx,10h 00401021 mov eax,0CCCCCCCCh 00401026 rep stos dword ptr
// 3: return 0; 00401028 xor eax,eax // 4: } 0040102A pop edi 0040102B pop esi 0040102C pop ebx 0040102D mov esp,ebp 0040102F pop ebp 00401030 ret
也就是这些代码,基本所有的函数都基于这一份汇编模板,只要我们记住他。 以后函数在我们手中, 就如同玩物。 好了,这节课我们知道了 函数入口main 自定义函数 C的汇编执行 下节课再见!

复习与巩固


C语言函数书写

函数类型 函数名(参数列表) { //函数体 return; }
函数就是一堆指令,可以完成我们重复使用的功能

堆栈图的画法

函数代码:
int jiafa(int a,int b) { return a+b; }
调用代码:
void main() { jiafa(4,5); return; }

我们通过最简明的方式,来了解,什么是参数?参数如何传递?什么是返回值?返回值放在哪?

http://static-v.zhishibox.net/2021227_qiniu_9ead0201323433a7a05fe9539eb42ce3.mp4
Tip:补充
http://static-v.zhishibox.net/2021227_qiniu_aa34bb5a8bc4bb3b4fc54ff7aa3a0f8d.mp4

if else语句

单if语句

此处的表达式为判断,例如:
int a = 1; int b = 2; if (a>b) //如果a>b { print("a>b"); //输出a大于b }

if(表达式) { 语句; }

if、else语句

那么我们如上语句,还要有一个不满足a>b的条件,就用到if/else,如下:
int a = 1; int b = 2; if (a>b) //如果a>b { print("a>b"); //输出a大于b } else { print("ab,则输出a
if(表达式) { 语句1; } else { 语句2; }

if、else if、else语句

如果我们有多个判断条件,就可以用if、else if、else语句
int c = 60; if (c>90) //判断C>90 { print("好棒哦"); } else if(C>=60) //不满足的话继续判断C是否>=60 { print("刚及格"); } else //以上判断都不满足 { print("这。。。。"); }

if(表达式1) { 语句1; } else if(表达式2) { 语句2; } else if(表达式3) { 语句3; } else { 都没判断到的语句; }

if 嵌套语句


if(表达式) { if(表达式) { 执行语句; } } else { if(表达式) { 执行语句; } }
看着上面复杂的代码可能我们会头疼,但是,我们嵌套一个实例来看。
#include void main() { //假设你要结婚了,女方家长问了你三个问题 int money; //你有多少存款啊? int horse; //你有多少房子啊? int old; //你多大了? printf("你有多少存款啊? \n"); scanf("%d",&money); //用于用户输入,也就好像你丈母娘问你,你要回答他问题,他把这信息记到了心里。 printf("你有多少房子啊? \n"); scanf("%d",&horse); printf("你多大了? \n"); scanf("%d",&old); if (money >= 100000) //判断有没有十万 { if (horse<2) //如果够了十万判断是否有两套以上的房子 { printf("有十万又怎么样,房子没两套就是穷\n"); //没有就执行 } else //如果有 { if (old<=25) //再问你多大了,是不是25以下 { printf("嗯,25之前有十万,又有两套房,也算是年少有为了,嫁给你吧 \n"); //嗯,有十万,又有房,又25一下,满足条件,迎娶美人 } else { printf("都那么大年纪了,才挣够,我再考虑下。\n"); //年龄超过了预算,有也不考虑你 } } } else { printf("有没有搞错?十万都没有 \n"); //嗯,没有十万块钱,直接pass } }
这样是不是就很好理解了。

if语句 在汇编层面是怎么实现的?

示例代码:
#include void main() { int a = 10; int b = 20; if(a>b) { printf("hello world \n"); } return; }
我们还是在return主函数结尾F9下断调试
00401028 mov dword ptr
,0Ah 0040102F mov dword ptr
,14h 00401036 mov eax,dword ptr
00401039 cmp eax,dword ptr
0040103C jle main+3Bh (0040104b) 0040103E push offset string "hello world \n" (0042501c) 00401043 call printf (00401180) 00401048 add esp,4

-1、首先 mov eax,dword ptr
这一步就把a的值放入了eax -2、cmp eax,dword ptr
这一步把a和b进行比较 -3、jle main+3Bh (0040104b) 小于等于b的话就跳转,jle汇编指令,可以回头看,不知道什么意思的话
可能你会疑问,我们写的 if a>b,可是为什么会小于等于。 a如果小于等于b,也就是小于等于20,就跳向0040104b,这地址是不成立的代码区域,所以逻辑是一样的
如果我们大于b,就会,如执行如下代码,也就是我们输出的hello world

0040103E push offset string "hello world \n" (0042501c) 00401043 call printf (00401180) 00401048 add esp,4

switch case语句

switch语句格式:
switch(表达式) { case 常量1: //如果达成条件1 语句; break; //跳出 case 常量2: 语句; break; default: //如果所有case都不满足 语句; break; //跳出 }

-1、表达式结果不可是浮点数 -2、case后的常量值不能一样,且必须为常量。 -3、注意case 常量后是":"冒号 -4、一定不要忘了break,如果没有break就会一直往下执行,一直看到break为止 -5、default语句不分位置,就是放到case上面,也不会影响执行顺序

条件合并写法

比如,case1和case2我要执行的语句是一样的
switch(表达式) { case 常量1:case 常量2: //两个case合并到了一起 语句; break; case 常量3: 语句; break; default: 语句; break; }

switch和if的区别


-1、switch只进行等值判断(直接一个值判断),if、else可区间判断(比如a>b、b>c) -2、switch执行效率高于if、else语句,分支越多越明显。

switch 语句原理

研究下switch为什么执行效率高于if、else


if、else执行汇编代码

示例代码如下:
#include #include void Mpr(int a) { if (a == 1) { printf("1\n"); } else if(a == 2) { printf("2\n"); } else if (a == 3) { printf("3\n"); } else if (a == 4) { printf("4\n"); } else { printf("default\n"); } return; } void main() { int c; printf("输入一个数值\n"); scanf("%d",&c); Mpr(c); }
汇编代码截图:
10: else if(a == 2) 0040104D cmp dword ptr
,2 //用2和输入的参数比较 00401051 jne Mpr+42h (00401062) //不成立就直接跳到00401062继续判断 11: { 12: printf("2\n"); 00401053 push offset string "2\n" (00425030) 00401058 call printf (00401160) //成立就输出对应语句 0040105D add esp,4 13: } 14: else if (a == 3) 00401060 jmp Mpr+79h (00401099) 00401062 cmp dword ptr
,3 00401066 jne Mpr+57h (00401077)
所以综上所述,if、else是在不停的判断跳转,很麻烦


switch、case执行汇编代码

示例代码如下:
#include #include void Mpr(int a) { switch(a) { case 1: printf("1\n"); break; case 2: printf("2\n"); break; case 3: printf("3\n"); break; case 4: printf("4\n"); break; default: printf("default\n"); break; } return; } void main() { int c; printf("输入一个数值\n"); scanf("%d",&c); Mpr(c); }
汇编代码截图:
00401038 mov eax,dword ptr
//把参数传入eax 0040103B mov dword ptr
,eax //eax存入ebp-4 0040103E mov ecx,dword ptr
//参数从ebp-4传入ecx 00401041 sub ecx,1 //参数-1,输入的参数为2,2-1=1
减去case值最小的值
00401044 mov dword ptr
,ecx //ecx放回ebp-4 00401047 cmp dword ptr
,3 //和3作比较 0040104B ja $L42253+0Fh (00401093) //如果比3大,跳转到00401093(也就是default) 0040104D mov edx,dword ptr
//不成立,参数1放入edx 00401050 jmp dword ptr
//跳转edx(1)*4+4010b1 = 4010b5
所运行如上图所示。这样就通过计算,通过一个jmp可以去任何地方 而不用像if一样,不断地进行比较跳转

while 循环语句

某些时候,我们想让某些语句按照一定条件反复执行,怎么去做? 比如:把0~10打印出来。 那么在汇编里,我们知道,想重复执行一块代码或者一句代码,用jmp反复跳转,不停回去执行即可 在C语言里,也有类似jmp的语句指令。
#include void main() { int a = 0; printf("%d \n",a); a++; }
如果我们的代码如上所示,他就会输出一个0,没法达到0~10打印的目的。
#include void main() { int a = 0; C: printf("%d \n",a); a++; if (a<=10) { goto C; } return; }
所以上面这种写法才是正确的。
-1、我们在输出上面声明了一个标签C,这个标签的名字可以随便写 -2、利用if判断是不是小于等于我们要输出到的数值,如果是,就goto(jmp)回去继续执行语句。
但是我们这样写很麻烦
-while语句 -do while语句 -for 语句
上面三种循环指令,可以为我们提供按照条件反复执行的操作。
#include void main() { int a = 0; //变量a while (a<=10) //while语句判断a是否小于等于10 { printf("%d \n",a); //输出a a++; //a自加,然后继续while判断是否成立 } return; }
如上代码,while就会判断a是否小于等于10,如果成立就执行,不成立就不跳回去继续执行了。 也就是如下格式:
while (条件表达式) { 执行语句; }

while语句嵌套

那么我们while语句是否可以嵌套其他语句?是可以的
//我们取出0~10中,所有的偶数 #include void main() { int a = 0; while (a<=10) { if (a % 2 == 0) //其他语句相同,嵌入if判断,取余2是不是等于0,等于0就是偶数 { printf("%d \n",a); //取余2等于0就输出。 } a++; //a自加,为什么不放在if里请看下面: /* 1、a默认为0,while判断a的确小于等于10 2、if判断,0取余2等于0,输出0,a自加 3、此时a为1,还是小于等于10,1取余2不为0,不进入if 4、while继续循环。 如此一来,就反复的不停循环,永远不会终止,这样叫做死循环 所以位置要放对 */ } return; }
那么,while循环也可以搭配以下两个关键词使用。
关键词|代码释义 break|跳出离着这个break最近的switch和while continue|直接返回到离着最近的while或者switch,下面的语句全不执行

#include void main() { int a = 0; while (a<=10) { if (a % 2 == 0) { break;//离着最近的是if上面的while,所以最后会什么也不输出,跳出while循环 } printf("%d \n",a); //最后还会输出a a++; } return; }


下面是一个有错误的语句,请检查


#include void main() { int a = 0; while (a<=10) { if (a % 2 == 0) { continue; //有了contine,下面代码全部会不执行,直接跳回while处 } printf("%d \n",a); //因为在continue下面,如果continue执行了,这一句包括a++全部不执行。 a++; } return; }


好了,我们最后来一个while嵌套while来终结本章内容


#include void main() { int a = 0; while(a<10) { int b = 1; while(b<=a) { printf("%d * %d = %d ",a,b,a*b); b++; } printf("\n"); a++; } return; }
看不懂没关系,利用F9下断,然后一句一句F10执行去观察代码执行顺序 这次不用反汇编窗口单步,就是代码窗口单步。

do while 循环语句


#include void main() { do { //执行代码 } while (/*判断表达式*/); //while判断条件 return; }
while和do while的区别:
-1、while会判断成立后才执行 -2、do while,会先执行一次代码,再去判断是否成立,也就是不管成立不成立,都先执行一次

#include void main() { int a = 0; do { a++; printf("%d \n",a); } while (a<=10); return; }
如果你执行上面的代码你会发现,输出了1~11,为什么?
-1、int a 声明变量a为0 -2、先执行,a自加 = 1 -3、输出a,此时a为1 -4、while判断是否小于等于10 -5、是,继续回到自加执行 -6、a加到10了,小于等于10,条件还是满足的 -7、a自加为11,输出,判断是否小于等于10,不成立,不执行 -8、但是因为最后又执行了一次,所以输出了11

for 循环语句


for (1、表达式1;2、表达式2;3、表达式3) { //4、执行代码 }


最简单的方式


for (1、循环变量赋初值; 2、循环条件;3、循环变量增值) { 4、语句; }
执行顺序如下:
-1、2、4、3 -2、4、3 -2、4、3


代码代入如下


#include void main() { for (int a = 0;a<=9;a++) { printf("%d \n",a); } return; }
代码的汇编执行图:
-1、首先绿色1,是第一个表达式,初始化变量 -2、蓝色2,是判断a是否小于等于9 -3、蓝色下面输出,是执行语句,也就是第四步 -4、最后才会去执行,橘色,a++自加
字节理解下,不懂问我,基本就是这个样子。

数组

C语言最重要的两个部分:
数组、指针
,所以一定认真学,如果这两个没学会,相当于白学!!!

什么是数组?什么情况需要使用数组?


-1、比如我们要定义一个变量,来存储一个人的年龄

void main() { int age; reurn; }
平时我们肯定会这样定义,但是这样只能存储一个,如果我们要存储n个人的年龄呢?
数组定义格式
数据类型 变量名
; 数据类型就是我们那些 int、long、short、double等 变量名符合命名规则可以随便写 中括号里的常量,要写定义多少个变量,比如我们存储5个人的年龄
void main() { int age
; //定义数组完成 reurn; }
数组就是一堆变量声明到了一起,没必要复杂害怕。
数组的初始化


未初始化的数组:

不给初始化数值的话,会使用堆栈默认填充的值:CC

初始化后的数组:


-1、如上图所示,1放入了ebp-14,2放入了ebp-10 因为我们定义在了函数内,局部变量,都是在堆栈中分配的,所以不是一个地址,而是一个寄存器寻址。 如果放到了全局,就不会是ebp-14 -10,而是一个绝对地址
初始化的两种方式

int age
= {1,2,3,4,5}; int age
= {1,2,3,4,5};
不管数组后面写不写数量,计算机都会根据后面赋值内容进行检测生成。

数组的读写

如果我们用了: int age
; 我们暂时不想让数组卡到某一个值,等用到的时候在进行赋值,我们应该怎么做?
void main() { int age
; return; }
等我们用到的时候可以用如下方式写入数据:
void main() { int age
; age
= 1; age
= 2; return; }
再来看下写入数据后的汇编代码。 和直接进行初始化的汇编代码一样。

为什么写age

数组下标从0开始,也就是5个数据的编号:0、1、2、3、4 如果写了5,就会抛出异常。

那么怎么读取数据?


void main() { int age
; age
= 1; age
= 2; int a; //定义变量a a = age
; //a接收数组中的第一个数据 return; }
这样就完成了数据的读取

多维数组

多维数组的定义:

比如,我们班级有5个小组,每个小组有2个人,我们要存储这5个小组,10个人的年龄。 1、比如我们上面写的单维数组,那么我们就只能这么写 int a
或者a
2、但是用多维数组,我们可以像以下方式进行书写 int a

那么再例如:

我们家里有一栋楼房,这栋楼房里有20个房间,每个房间里面住了8个人 我们就可以写 int a
或者int a


int a


就叫做多维数组

那么二维数组如何像一维数组那样进行初始化?


int a
= {0,1,2,3,4};
我们都知道一维数组像如上方法进行初始化 那么
二维数组呢?
还是,我们有3个房间,每个房间里面2个人
int a

={ {16,18}, {24,27}, {57,65} };
那么上面这样初始化是不是很蒙?别怕,我来给你们解释一下:
-1、三个花括号,代表我们的三个房子,最大的那个花括号,代表整体 -2、每个房子里面有2个人,所以,我们给三个房间里的人分配了年龄。

int a

={ //大整体 {16,18}, //每个括号就代表每个房子 {24,27}, //每个房子里面两个人,所以24,27年龄。 {57,65} };

二维数组在内存的布局

不管几维数组,在内存中的布局,全都是连续存储。 就像这样,三个房间,每个房间两个人,连续存储方式。
别困惑
首先来看一维和二维数组的汇编代码:
// int a
= {1,2,3,4,5,6}; 0040D728 mov dword ptr
,1 0040D72F mov dword ptr
,2 0040D736 mov dword ptr
,3 0040D73D mov dword ptr
,4 0040D744 mov dword ptr
,5 0040D74B mov dword ptr
,6

/* int a

= { {1,2}, {3,4}, {5,6} }; */ 00401028 mov dword ptr
,1 0040102F mov dword ptr
,2 00401036 mov dword ptr
,3 0040103D mov dword ptr
,4 00401044 mov dword ptr
,5 0040104B mov dword ptr
,6
观察以上两段反汇编,是不是发现多维数组和一维数组并无区别? 那么都一样,为什么还要使用多维数组? 比如,a
= {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20} 这样取出数据的时候可能你知道是第几个,如果乱序呢?是不是要自己算? 如果 a

={ {1,2,3,4,5}, {6,5,4,3,2}, {2,5,6,9,7}, {1,9,4,7,7} }; 这样多维数组去取数据,我们要取出第十四个数据,那么五个一列就是第三列的第四个 int b = a


为什么不是a

?你们还记得吗?

二维数组的读写

比如: 本课程一共有5章,每章10课
int kecheng

={ {1,2,1,3,5,6,8,9,7,4}, //下标0 {9,6,3,4,5,8,6,4,2,3}, //1 {7,9,5,4,3,8,1,4,9,6}, //2 {8,6,3,4,2,1,9,9,8,6}, //3 {3,4,8,5,1,4,7,9,6,3} //4 };
那么我们想取第4章的第7课怎么写? a

我们想看第1章的第1课 a

那么我们的数据存到内存中,都是连续存储的,不会像我们的代码一样,很容易看懂! 编译器是怎么帮我们找到这个数据的? a
= a
也就是数组的第36个数据 那么我们核对一下,第一排0~9,第二排10~19,第三排20~29,第四排30~39,那么也就是1后面的那个9,第36个数 a
= a
这个就是0,下标全是0,就是第一排的第一个,1 这就是编译器帮我们找数值的算法。
上面就是我们多维数组的本质,对我们来讲,多维和一维数组都是等价的,只不过是可以在使用的时候看哪个更方便。
再来一个例子:
我们有5章节,一章4节课,每节课有3个人上 那么多维数组:
int a


= { {{1,2,3},{4,5,6},{7,8,9},{9,8,7}}, {{3,2,1},{6,5,4},{9,8,7},{7,8,9}}, {{2,1,3},{5,6,4},{9,7,8},{8,7,9}}, {{3,1,2},{6,4,5},{8,7,9},{9,7,8}}, {{1,3,2},{5,4,6},{8,9,7},{7,9,8}} };
那么这里我们就也知道为什么平时我们使用多维数组了吧,如果此处用一维数组 5*4*3=60个,我们要一个一个找就很头疼。 但是我们用多维数组 我们想看看第1章节的第3节课,第三个观看的人是谁 那么就是a


还是因为下标是0开始
编译器如何计算?
a


第一章节,第三节课,第三个人 a
那么我们想查看第3章节的,第2节课的,第1个人。 编译器公式:a
课后:
想一下,如果我们想看第4章节的,第4节课的,第3个人,公式是怎么样的? 编译器公式:????

进制

为什么学习进制?

计算机只认识二进制,也就是0和1,想要更好的学逆向,我们首先要理解什么是进制 计算机中存储的任何文件,接收的任何指令都是由0和1组成的。 我们平时在做逆向时看到的:
汇编代码:mov eax,0 十六进制代码:0xB8 0x00 0x00 0x00 0x00
这些都只是为了方便我们记忆观看,如果计算机为我们显示:
1011 1000 0000 0000 0000 0000 0000 0000 0000 0000
是不是很难看懂?甚至都不知道什么意思? 所以,汇编语言是学习逆向的必备之路!

怎么去学

学不好进制,是因为很多人都用十进制去考虑其他进制 我们暂时先抛弃掉原来十进制记忆其他进制的方法,跟着本课程慢慢往下学。

进制定义

八进制:八个符号组成,0、1、2、3、4、5、6、7,逢8进1 十六进制:十六个符号组成,0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F,逢16进1

进制书写方式

比如:
0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F
这是十六进制的十六个数,如果想继续写15以后的数,该如何做? 那么十六个数就无法表示了,就得进位,就好像9+1 = 10 F=15无法单位表示了,因为二进制没有二,八进制没有八,十六进制没有十六 先用00占位,F后面没有数值,就进位看回了第一位。 第一位0,产生进位 ,0往前看一位是1,所以十六用十六进制表示就是10
那么自己动手用十六进制写一下,16-32,然后在群里发给我
进位图:
课后习题
1、课后请用二进制,将0000~1111写出来

新增 - 进制的运算

进制的本质就是找对应数值,每个进制都是一个独立体系,并不需要转换成其他形式做运算。

八进制运算

2+3 =? 2*3= ? 4*5=? 277+333=? 276*54=? 237-54=?234/4=? 1、先写一组八进制数 0、1、2、3、4、5、6、7、10、11、12、13、14、15、16、17、20、21、22、23、24、25、26、27、30

计算:

2+3=5 2*3=找两个3,或者找3个2,也就是1/2、3/4、5/6或者1/2/3、4/5/6,所以最后=6 4+5=在4后面数5个数,=11 4*5=4个5查,或者5个4查,=24

乘除法

1、首先先将加法表和乘法表列出来
加法表: 1+1=2 1+2=3 2+2=4 1+3=4 2+3=5 3+3=6 1+4=5 2+4=6 3+4=7 4+4=10 1+5=6 2+5=7 3+5=10 4+5=11 5+5=12 1+6=7 2+6=10 3+6=11 4+6=12 5+6=13 6+6=14 1+7=10 2+7=11 3+7=12 4+7=13 5+7=14 6+7=15 7+7=16

乘法表: 1*1=1 1*2=2 2*2=4 1*3=3 2*3=6 3*3=11 1*4=4 2*4=10 3*4=14 4*4=20 1*5=5 2*5=12 3*5=17 4*5=24 5*5=31 1*6=6 2*6=14 3*6=22 4*6=30 5*6=36 6*6=44 1*7=7 2*7=16 3*7=25 4*7=34 5*7=43 6*7=52 7*7=61

计算


277+333

1、7+3 找表=12,留2进1 2、7+3 找表=12,留2进1,因为第1步有一个1,所以留3进1 3、2+3 找表=5 , 进位+1=6 4、最后=632

276*54

-从乘法表里找,276*4 = 1370 -乘法表里找,276*5 = 1666
记得最后是错位相加
234/4 = ????

1、看谁*4≈23,看到4*4=20,写4剩34 2、继续看表谁*4=34,是7,写0剩0
所以最后=47
我们上节课,学习了二、八、十六进制,以及各个进制的进位和书写方式 这节课,我们学习一下数据宽度

数据宽度

计算机存储数据,不是无限制存储数据的 就好像我们都知道的,一个水杯是有容量的,当你接的水超过了可以容纳的量以后,就会流出来 计算机也是这样,如果存储的数据超过了所能接受的范围,就会选择抛弃一些数据来存储新的数据。这就是数据宽度(可以理解为一个容器) 常见的单位有如下几个:
-位 Bit 只存一位二进制 -字节 Byte 可存储八位二进制0~FF -字 Word 可存储十六位二进制0~FFFF -双字 Dword 可存储三十二位二进制0~FFFFFFFF
日常使用计算机时,最经常看到的就是字节。右键一个文件属性,就会显示如下界面: 一个字节所能存储的内容就是八位二进制

数据溢出

那么如果超出存储范围,会发生什么事情?看下图: 先不用看懂代码的意思,我带你们来解读一下:
-int 整数型,四字节存储单位,可存储八位十六进制,三十二位二进制 -我们代码中给了九位十六进制,也就是三十六位二进制,已经超出了可承受范围。 -内存窗口,0x19FF3C地址的数据为2B FF FF FF,先不用了解数据为什么反过来显示,涉及到的存储模式后面讲 -但是我们可以看到的是,A没了,所以,如果超出存储范围,就会抛弃数据,默认抛弃左边的(高端)数据 -至于前面的0x没显示不是因为数据抛弃高端数据,而是0x在编程中代表十六进制的意思 -0b代表二进制,0代表八进制,十进制什么都不用输入
可以自己去动手用Visual Studio C++ 6.0 或者 Visual Studio 2019 Pro 这两个IDE按照上面的代码去调试下

总结

总结:本节课我们学习了什么?
-数据存储的常见单位和宽度 -如果数据超出可承受范围所发生的事情 -二、八、十、十六进制在编程中的字母表示

如果数据超出存储范围,抛弃哪些数据?|右侧数据|!左侧数据|随机数据|自适应宽度

指针类型

C语言中最难的一部分,
指针
,来了~
int a; //定义整数类型a float b; //定义浮点类型b char c; //定义char类型c

那么上面几种写法我们都认识了,我们可以来看下面的


int* a; float* b; char* c;

我们之前学过所有的类型,都可以如上加上一个*

加上*之后,就变成了一个新的类型,统称为“指针”
指针的*号可以是多个,但是加一百个,也叫指针,但是一个和多个是有差异的,以后会知道

如何赋值

我们都知道
int a; a=1;
我们原来都是这样,赋值的,但是这个样子是简写的
int a; a = (int)1;
这样的写法是完整的写法。但是编译器可以自己识别,所以我们不用写全。
int* a; short* b; char* c;

我们如果是指针类型,就不能用简写方式进行赋值了,我们应该像下面这样赋值

a = (int*)5; short = (short*)1; char = (char*)1;
定义指针有多少个*,就给括号里写多少个,要对应起来。

可不可以给两个相同的指针变量赋值?

可以
int** a; int** b; a = (int**)6; b = a;
这样子是可以的,但是要确保两个指针相同。

指针变量宽度

指针对于C语言,相当于一个新的类型。 如下代码下断调试反汇编内容:
#include #include void main() { char* a; short* b; int* c; a = (char*)1; b = (short*)2; c = (int*)4; system("pause"); return; }
我们发现char,short,都成为了dword,4字节 那么如果我们有多个*,会有影响吗?

不会,指针类型永远是4字节,不管是什么类型,不管是有多少*号

指针的自加自减运算

普通的基本类型 int a; a++; 这种代码我们已经能明白了 他就是每次自己+1,或者-1
那么指针类型呢?
也可以
#include #include void main() { char** a; short** b; int** c; a = (char**)5; b = (short**)5; c = (int**)5; a++; b++; c++; printf("%d %d %d \n", a, b, c); system("pause"); return; }

我们发现如下图所示,都变成了9



现在++都+了4,--也一样,都会-4。

但是如果我们只有一个星星呢?

我们发现只有一个星的时候,都是加了自身的类型宽度 char是1字节,5+1=6 short是2字节,5+2=7 int是4字节,5+4=9
所以我们得出以下结论

-++或--时,会加或减,当前"*"星数去掉一个后类型的宽度(比如我们上面的例子,一个*,去掉了就是普通的char,int,short,对应着1字节,4字节,2字节) -非指针的类型,也就是不带*的数据类型,++或--,就默认是+1或者-1

指针类型可以做加法、减法,但是不可以做乘除运算

char* a; short* b; int* c; a = (char*)5; b = (short*)5; c = (int*)5; a = a + 5; b = b + 5; c = c + 5;


那么我们如果,不让他自加了,开始都加5,那么结果是多少?

可能又和我们想的不同,因为1个星,去掉之后字节宽度还有1、2、4 1+5=6、2+5=7、4+5=9,那么为什么出来了10、15、25

看如下解答

因为char去掉*还有1字节,已经赋值给a为5 所以 a = 5 + 1*5 =10 b就 = 5 + 2*5 = 15 c就 = 5 + 4*5 = 25

所以规律如下:

指针变量 + N(要加上的数值) = 指针变量 + N*(去掉*号后指针的宽度) 就像 char* a; a = (char*)5; a = a + 5; 那么指针变量就是a,n就是5 a+5 = a本来就为5 + 5*1,因为char*去掉1个后,为1字节 a = 5+5 = 10

减法规律如下:

指针变量 - N(要加上的数值) = 指针变量 - N*(去掉*号后指针的宽度) 就像 char* a; a = (char*)5; a = a + 5; 那么指针变量就是a,n就是5 a-5 = a本来就为5 - 5*1,因为char*去掉1个后,为1字节 a = 5-5 = 0
所以按照我们的思路
a = a + 5; b = b - 5; c = c - 5; a = 5 - 5*1 = 0 b = 5 - 5*2 = -5 c = 5 - 5*4 = -15 印证:

指针类型的比较


#include #include void main() { int* c; int* d; c = (int*)5; d = (int*)10; if (c < d) { printf("1 \n"); } else { printf("2 \n"); } system("pause"); return; }

现在看着指针和普通类型,无法从汇编角度区分,以后我们会学到如何辨别是指针还是普通类型

所谓的指针代码等,都是写给编译器看的,真正编译出来的时候,起始差异不是很大,但是还是有差异的。
我们也看到,比较用的jae/jbe,这两个是判断无符号的,所以指针严格来讲,是个无符号的类型

指针禁忌

不要去乱自己发挥想象

别什么:指针里面存的是地址。

指针和地址没有很大的关系,他就是一个类型,想存什么存什么,想干什么干什么 只不过有几个特性:
-加加 / 减减 与其他不同 -有着自己的宽度 -也可以作比较

函数调用约定

就是编译器三件事:怎么传递参数、返回值,如何平衡堆栈
#include #include int met(int a, int b) { return a + b; } void main() { int c = met(1, 2); return; }
堆栈传参,从右向左传递 返回值,存于eax 外平栈

这三个步骤方式,都是由编译器决定的

如果我们没有声明调用约定 默认使用
__cdecl
调用方式,从右到左入栈,调用者(也就是调用函数的地方,比如再main函数里调用的这个函数,main函数就称为调用者)清堆栈
__stdcall
调用方式,从右到左入栈,自身清理堆栈
int __stdcall met(int a, int b) //改为stdcall模式 { return a + b; }
参数传递没变化 结果依然存于eax 但是堆栈平衡,采用了内平栈
__fastcall
,ECX/EDX传送前两个,剩下的从右到左入栈,自身清理堆栈 寄存器传参。ecx和edx 结果还是存放eax 内平栈方式。

__fastcall的效率比stdcall和cdecl效率要高,因为从CPU中读数据比内存中读数据要快

但是参数超过两个,fastcall的意义就没有那么大了,因为还是push堆栈传参了
stdcall基本用在windows提供的api上

平时写代码基本都用cdecl
以上三种是常见的调用约定,满足日常即可 深入了解参考微软提供帮助
2、有无符号数,3、位运算,5、通用寄存器,4、环境OllyDbg的基本配置,6、通用寄存器视频闯关,7、内存与内存地址,8、存储模式,9、寻址方式与巩固练习,10、常用汇编指令,11、堆栈,12、修改EIP,13、函数,14、寻址方式,15、汇编毕业章,JCC指令,000、第一个C语言程序,001、C语言的参数和返回值,002、(需补)变量,003、函数嵌套调用,004、数据类型,005、运算符与表达式,006、分支语句,007、数组,008、结构体,009、字节对齐,010、结构体数组,0、初识进制,1、数据宽度,011、最难-指针类型,012、&符号的使用,013、取值运算符,014、数组参数传递,015、指针数组 / 数组指针,016、调用约定,017、快乐的函数指针,018、C语言毕业:预处理,01、C++:新的一年新企航,02、This 指针,03、构造/析构函数,04、继承,05、类成员的访问控制,06、堆中建对象,07、引用类型,08、虚表,09、运算符重载,010、模板,011、纯虚函数,012、对象拷贝,013、内部类,014、命名空间,015、static关键字,001、WIN32基础,02、C语言中的宽字符(Unicode),03、WIN32API中的宽字符,04、进程的创建过程,05、进程创建,06、句柄表,07、进程相关的API,08、创建线程,09、线程控制,010、临界区,011、互斥体,012、事件,013、窗口本质/消息队列,014、写一个Windows程序,015、消息类型,016、子窗口,017、虚拟内存与物理内存,018、私有/共享内存的申请释放,019、文件系统,020、内存映射文件,021、静态/动态链接库,022、隐式链接,023、远程线程/注入,024、进程间通信,025、模块隐藏,01、MFC本质,02、第一个MFC程序,03、MFC初始化过程,04、MFC运行时类型识别(RTTI),05、MFC六大核心机制动态创建,06、消息映射,07、命令传递,08、MFC分析,09、MFC GDI基础,010、GDI,011、MFC GDI,012、鼠标和键盘,013、键盘消息,014、对话框,015、MFC控件+ListBox,016、ClistCtrl,017、TreeList,018、MFC文件和资源操作,019、MFC多页面设计,01、网络基础,02、Socket基础和TCP模型,03、UDP,04、阻塞/并发/非阻塞式模型,05、Select,06、WSAAsyncSelect,07、OpenSSL编译,08、RSA加密算法,01、数据库,02、数据库和表,03、增删更新表,04、单表查询,05、连接mysql,01、数据结构项目(前),02、数据结构概念,03、算法,04、时间复杂度,05、线性表的顺序/链式存储结构,06、静态/循环链表,07、栈的顺序/链式存储结构,08、队列,09、队列,010、串,011、树的简介,012、二叉树基础/遍历,013、线索二叉树,014、图,015、顺序查找,016、二叉排序/平衡树,017、多路查找树,018、哈希查找表,019、插入交换/选择归并排序,01、STL概述_Vector,02、Debug_List,03、Set,04、Map,05、Stack_Queue,06、算法,07、迭送器,01、硬编码,02、前缀指令,03、定长指令与变长指令,04、定长-修改ERX,05、定长-修改EIP,06、变长-ModRM,07、变长-RegOpcode,08、变长-SIB,01、PE结构,02、PE文件的两种状态,03、DOS头属性,04、标准/扩展PE头属性,05、PE节表,06、RVA与FOA转换,07、空白区添加代码,08、扩大/新增/合并节,09、导出表,010、导入表_确定依赖模块/函数,011、导入表_确定函数地址,012、重定位表,013、注入ShellCode,014、VirtualTable_HOOK,015、IAT HOOK,016、INLINE HOOK,016、HOOK攻防/过检测,01、保护模式,02、段寄存器结构/属性探测,03、段描述,04、段权限,05、代码跨段,06、长/短调用,07、调用门,08、中断门,09、陷阱门,010、任务段,011、任务门,012、分页,013、PDE,014、页目录表/页表基址,015、分页,016、TLB,017、中断与异常,018、控制寄存器,019、PWT_PCD,学到这的小惊喜,知识盒子,知识付费,在线教育