C语言解剖 - 基本语法

llgang

贡献于2012-08-15

字数:4880 关键词: C/C++开发

 上次我们讲完了 一个HelloWorld 程序的前前后后, 按照正向学习C的过程, 我们是应该学C的基本语法现象了... 其实 搞逆向 和搞正向学习过程是差不多的~ 而且逆向学好了, 很多纠结的C语言语法语法也就显得自然了. 如果做到了 对你的C代码对应的汇编都了如指掌, 你觉得还会有bug藏身么? 笔者抱着这个心态, 在学习期间, 认真学习了C逆向的一些知识. 下面几期中, 我将将这些东西陆续分享给大家. //--------------------------------------------------------------- //-----------------------C基本数据类型--------------------------- //--------------------------------------------------------------- C语言是一个弱类型的语言. C程序员如果想要写出优秀的程序, 必然是要"面向内存的". 我不能想象 一个对C语言基本数据类型不清楚, 对C程序内存分布不清楚 的人, 能写出有点用的C程序出来... 更不能想象一 个连 正向写个有用的程序都写不出来的人, 会搞逆向搞好.... 或许他能搞好, 至少他可以学会"爆破".. 呵呵, 开玩笑. C的基本数据类型: 1.基本数据类型是C内部预先定义好的数据类型,在定义变量时说明变量名和 数据类型,编译器会为变量分配指定的内存空间。数据类型的描述确定了数据 在内存中所占空间的大小,也就确定了数据的取值范围。 2.在C中,经常用到的基本数据类型有如下几个: 3.int(整型)、float(单精度浮点型)、 char(字符型) 。 short long .... 4.基本数据类型最主要的的特点是,其值不可以再分解为其它类型。 C并没有统一规定各类数据的精度、数值范围和在内存中所占的字节数, 各C编译系统根据自己的情况作出安排。 现在一般有这个规矩: short < int <= long == 4 byte. short 2 byte float 4 byte double 8 byte 另外, 再什么 无符号型数(unsigned), 有符号性(signed). 网上有资料, 很简单.. 关于"补码" 这东西~ 读者也需要花费写精力弄清楚... 否则以后会很吃亏待... 下面详细说下 浮点型在内存中的表示:(也必须弄懂浮点型表示法) 浮点数分float和double,分别为32位和64位。 重点:浮点数与二进制的转换 符号位 指数 尾数 长度 float 1 8 23 32 double 1 11 52 64 基本上float和double的转换二进制数的方法大同小异 这里只拿float来做转换 0 | 000 0000 0 | 000 0000 0000 0000 0000 0000 符号位 指数位 尾数位 例: 41 5E 00 00 => 13.875f => 0100 0001 0101 1110 0000 0000 => 0|100 0001 0|101 1110 0000 0000 符号位为0,是正数,指数等于100 0001 0 - 127 => 130 - 127 => 3 那么指数位结合尾数位 1.101111 * 2e3 => 1101.111 => 13.875f 注 为什么101 1110 0000 0000得到1.101111? 可以从下面的例子看出实数部分的1可以省去,需要从二进制转换成浮点数时, 再加上计算即可。 小数转换二进制方法 0.875 0.125 2 2 ----- ----- 1.750 1 0.250 0 0.750 2 2 ----- ----- 0.500 0 1.50 1 2 0.50 ----- 2 1.000 => 0.001 ----- 1.00 => 0.111 float i = 13.875f; => 转换二进制 拆分 13 => 1101 组合 1101.111 => 1.101111 * 2e3 0.875 => 0.111 符号位0,指数位127+3 => 100 0001 0, 尾数位省略实数的1 => 101 1110 0000 0000 综合 => 0100 0001 0101 1110 0000 0000 => 41 5E 00 00 float i = -0.125f; => 转换二进制 拆分 0 => 0 组合 0.001 => 1.000 * 2e-3 0.125 => 0.001 符号位1,指数位127-3 => 011 1110 0, 尾数为省略实数的1 => 000 0000 0000 0000 综合 => 1011 1110 0000 0000 0000 0000 => BE 00 00 00 (建议能强制自己弄懂基本数据类型, 和补码 与浮点表示法. 逆向是一个艰辛的路, 这就是我们的第一个砍了....) //--------------------------------------------------------------- //-----------------------基本语法现象的逆向---------------------- //--------------------------------------------------------------- 先来个最经典的逆向入门程序吧. 不运行, 先认真看如下程序: // [6/3/2011 MoAsm] #include int main(int argc, char* argv[]) { int nArray[10]; /*给数组nArray初始化为全零*/ for (int i = 0; i <= 10; i++) { nArray[i] = 0; } printf("初始化完成!\r\n"); return 0; } 有人说, 这个运行的结果是个死循环, 你同意吗? 如果你没能看出来, 那么上机试试吧. 为什么他会是个死循环? 且看程序Debug版本的反汇编代码: 7: // [6/3/2011 MoAsm] 8: #include 9: 10: int main(int argc, char* argv[]) 11: { 00410650 push ebp 00410651 mov ebp,esp 00410653 sub esp,6Ch 00410656 push ebx 00410657 push esi 00410658 push edi 00410659 lea edi,[ebp-6Ch] 0041065C mov ecx,1Bh 00410661 mov eax,0CCCCCCCCh 00410666 rep stos dword ptr [edi] 12: int nArray[10]; 13: 14: /*给数组nArray初始化为全零*/ 15: for (int i = 0; i <= 10; i++) 00410668 mov dword ptr [ebp-2Ch],0 0041066F jmp main+2Ah (0041067a) 00410671 mov eax,dword ptr [ebp-2Ch] 00410674 add eax,1 00410677 mov dword ptr [ebp-2Ch],eax 0041067A cmp dword ptr [ebp-2Ch],0Ah 0041067E jg main+3Dh (0041068d) 16: { 17: nArray[i] = 0; 00410680 mov eax,dword ptr [ebp-2Ch] 00410683 mov dword ptr [ebp+eax*4-28h],0 18: } 0041068B jmp main+21h (00410671) 19: 20: printf("初始化完成!\r\n"); 0041068D push offset string "\xb3\xf5\xca\xbc\xbb\xaf\xcd\xea\xb3\xc9!\r\n" (00426ee8) 00410692 call printf (00410920) 00410697 add esp,4 21: 22: return 0; 0041069A xor eax,eax 23: } 抽出关键代码: 12: int nArray[10]; 13: 14: /*给数组nArray初始化为全零*/ 15: for (int i = 0; i <= 10; i++) 00410668 mov dword ptr [ebp-2Ch],0 0041066F jmp main+2Ah (0041067a) 00410671 mov eax,dword ptr [ebp-2Ch] 00410674 add eax,1 00410677 mov dword ptr [ebp-2Ch],eax 0041067A cmp dword ptr [ebp-2Ch],0Ah 0041067E jg main+3Dh (0041068d) 16: { 17: nArray[i] = 0; 00410680 mov eax,dword ptr [ebp-2Ch] 00410683 mov dword ptr [ebp+eax*4-28h],0 18: } 0041068B jmp main+21h (00410671) 可以看到循环主要用这么几条指令来实现: mov进行初始化。 jmp 跳过循环变量改变代码。 cmp 实现条件判断,jge 根据条件跳转。 用jmp 回到循环改变代码进行下一次循环。所以结构.大体如下: mov <循环变量>,<初始值> ;给循环变量赋初值 jmp B ;跳到第一次循环处 A: (改动循环变量) ;修改循环变量。 … B: cmp <循环变量>,<限制变量> ;检查循环条件 jgp 跳出循环 (循环体) … jmp A ;跳回去修改循环变量 在这个循环中,i的的地址为dword ptr [ebp-2Ch], 循环中 对数组做初始化, 这个一维数组的寻址公式为:dword ptr [ebp+eax*4-28h]. 观察上面代码, 我们发现, ebp 是当前函数栈的基址, eax 是[ebp-2Ch](i的地址) 所以数组的寻址公式就是: &nArray[i] == dword ptr [ebp+i*4-28h] == dword ptr [ebp+[ebp-2Ch]*4-28h] 而当 i == 10 的时候, 数组寻址就发生了越界! 越界也"巧合地" 越到了i的地址上: 当 i == 10时候, &nArray[10] == dword ptr [ebp+10*4-28h] == [ebp-2Ch] == &i. 好了. 这样, i在每次 == 10 的时候, 就又因为 数组的越界 而被置0了, 这个循环是永远出不来了... 后头想想, 不对劲: 你一定想问我, 我是怎么知道越界就正好越界到i处的? 原因在于 局部变量的内存分布特点了. 函数体内定义的局部变量(不含静态局部变量) 的分布规律: 先定义的变量a 地址在 ebp+sizeof(a) 这个地址上, 后定义的变量, 在此基础上"像内存低地址生长".例如上面的程序内存分布为: 所以”局部变量, 先定义在高地址, 后定义在低地址.” 看上面的内存分布, 会发现: & I == nArray[10]. 因为C语言是不检查数组越界的, 所以, 我们就非常顺利地 修改了i的值而没有获得任何警告! 好了, 上面的程序看懂了, 再看一个好玩的程序: // 不要数组 实现数组. #include #include void main() { int array; __asm { lea eax, [esp] mov array, eax; } printf("你可以随便输入字符(40个以内):\r\n"); scanf("%40s", (char*)array); printf("你输入的是:\r\n"); printf("%s\r\n", (char*)array); } 程序的意图是, 借助debug版的函数, 即使在你没有定义局部变量, 函数栈也会被开辟40h大小的原理, 把这部分栈空间当做 一个数组来看待. 我想, 弄懂了上面我说的额汇编代码的人, 应该很容易看懂我这个程序吧? 废话不多说了~ 大家自己动手调试吧~ 还是那句话: 汇编之前, 了无秘密. 好了, 今天就到这儿了. 下次我们将分析C的集中基本语法现象: 循环,分支, 结构体, 枚举…. 下面先热热身, 预览如下程序, 并尝试看懂其debug下的反汇编代码. // [6/3/2011 MoAsm] #include #define for if(0); else for int main(int argc, char* argv[]) { int nCount = 0; for (int i = 1; i <= 10; i++) { nCount += i; } printf("1到10的和为:%d\r\n", nCount); return 0; }

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 5 金币 [ 分享文档获得金币 ]
0 人已下载

下载文档

相关文档