C语言格式字符
c 输出单个字符
d 以十进制形式输出带符号整数(正数不输出符号)
e 以指数形式输出单、双精度实数
E 以指数形式输出单、双精度实数
f 以小数形式输出单、双精度实数
g 以%f%e中较短的输出宽度输出单、双精度实数,%e格式在指数小于-4或者大 于等于精度时使用
G 以%f%e中较短的输出宽度输出单、双精度实数,%e格式在指数小于-4或者大于等于精度时使用
i 有符号十进制整数(与%d相同)
o 以八进制形式输出无符号整数(不输出前缀O)
p 指针
s 输出字符串
x 以十六进制形式输出无符号整数(不输出前缀OX)
X 以十六进制形式输出无符号整数(不输出前缀OX)
在初学者刚接触C语言时, 经常会在控制台下出现“烫”字符, 也就是出现数组访问越界的情况。本文从这个问题出发, 通过反汇编技术来分析, 从底层内存角度揭开此异常的根本原因。
1 反汇编技术基本介绍
1.1 80×86系列芯片发展历史
80×86系列芯片是英特尔公司的前期产品, 第一个是8086芯片, 拥有内外16根数据线, 20根地址线。所以, 8086乃至后来的80286, 都是以段地址+偏移地址为寻址方式的, 它的寻址空间只有一兆, 后来出现了80386芯片, 地址线和数据线都扩展为32根, 每次可以处理32位数据, 可寻址空间变为2^32次方, 也就是4G空间。同时, 增加了实模式, 保护模式, 虚拟8086模式, 为用户提供了更多选择, 也基本奠定了今后的处理器芯片市场发展趋势。所以, 如果在VC6.0下按F10进入调试模式, 然后按ALT+8查看反汇编代码, 则可以看到类似于0×2343213A这样的数字, 这就是程序所在的内存地址的标号。因为该数字是16进制, 根据16进制和2进制的对应关系, 该数字转成2进制, 则是一个32位数值, 正好对应于芯片的32根地址线。
1.2 函数栈帧
在计算机中, 有一个特殊的栈空间, 遵循先进后出, 后进先出的方式, 由ebp表示栈底, esp标识栈顶。
在C语言中, 参数传递有两种方式, 一种是值传递, 一种是引用传递, 例如, 简单的求和函数sum (1, 2) , 实参1, 2传给了形参, 这就是值传递, 实参传给形参, 并不影响自己, 如图1, 参数1, 参数2分别压入栈中, 然后将本指令下一条指令的地址压入栈, 作为返回地址, 然后开始sum函数的调用, 如图1所示。
2 一段访问越界函数的具体分析
这段函数的作用很简单, 意思是输入一个字符数组, 然后依次打印出数组的各个元素。然而运行时, 却出现了这样的结果, 如图2所示。
gets函数的作用是从输入缓冲区中读取一个字符串, 赋给a字符数组。然而, 在输入时输入的字符长度并不到a的长度, 但后面的输出程序却要依次把a数组的元素打印出来, 这样就会引起访问越界, 会把SFA后面的未知字符打印出来。
按F10进入调试模式, 然后按ALT+8查看反汇编代码, 可以看到每个语句对应的反汇编语句, 下面开始具体分析。
栈顶指针寄存器esp自减58h, 申请了一个大小为58h的栈空间, 栈空间最小为40h, 此处的局部变量大小为58h-40h=18h为24字节大小, 对应于a数组的20字节和i变量的四个字节, 一共24字节的局部变量。
语句是将16h赋给ecx寄存器, ecx寄存器是循环次数寄存器, 这里是16h也就是22次, 第二条语句意思是将0x CCCCCCCC赋给eax寄存器。
语句是使用rep stos语句将eax通过循环分别赋给edi所指向的内存空间, 这里循环次数是16h也就是22次, 每次都是把四个字节CCCCCCCC赋给edi所指向的栈空间, 每次循环edi都要加上4, 所以一共对88个字节进行赋值, 正好等于16进制的58h。所以, 这三行语句意思是将本函数的栈空间初始化为0x CC。
本段意思是调用gets函数, 读取一个数组, 放入函数栈空间, 其首地址应该是栈内a数组的首地址, 也就是ebp-14h这里。所以, 这里把ebp-14h这个地址赋给了eax寄存器, 然后调用gets, 在gets函数中, 读入的字符串都进入了eax所指向的地址, 也就是a数组。
最后一句add esp, 4, 涉及到_stacall, _cdecl调用方式, 第一种是函数自己进行堆栈平衡, 本题中压入的实参影响了堆栈区。所以, 在调用结束时, 要add esp 4, 把esp指针抬高四个字节, 让它重新回到原来的main函数栈顶。
第二行意思是跳转到0x0040b786处去执行, 可以看到该处的代码是cmp dword ptr[ebp-18h], 14h, 也就是将i与20进行比较, 再往下执行, 会看到jge main+54h。如果≥20, 就跳转到0x40B7A4处, 也就是程序末尾, 跳出循环。而如果不大于, 就往下执行, 也就是printf语句。
printf语句对应的反汇编代码是从0x40B78C开始的, 第一步是movedx, dword ptr[ebp-18h], 将i的值赋给edx, 如第一次, edi值为0。
第二步movsx eax, byte ptr[ebp+edx-14h], 也可以看作movsx eax, byte ptr[ebp-14h+edi]。首先ebp-14h是数组的首地址, 数组的地址是从小往大增长的, 第一个元素地址是ebp-14h+0, 第二个元素是ebp-14h+1, 以此类推。所以, 本语句是通过edi的改变, 依次访问数组的每个元素, 第一次本语句执行完后, eax存放的就是数组的首元素。
接下来是将该元素压入栈中, 然后调用printf函数, 恢复堆栈。
往下走, 会看到jmp main+2Dh (0040b77d) , 也就是跳转到mov ecx, dword ptr[ebp-18h], 将i的值再次送入ecx中, 然后add ecx, 1, ecx自增1, 然后再送入i的位置, 这样i的值就自增了1, 然后再和20相比, 如果没有符合jge, 则执行printf语句, 此时再把i的值赋给edi, edi就变成1了, 而不是0了, 这样打印出来的就是下一个字符。依次类推, 一直到≥20, 跳出循环。
这段代码意思是弹出开始压入的edi, esi, ebx, 回复现场, 然后esp加上58h, 栈顶指针恢复到初始位置, 然后比较ebp于恢复后的esp, 调用_chkdsk, 检查堆栈是否被平衡, 然后回复esp指针, 弹出ebp, 现场恢复完毕。
3“烫”字符的出现以及原因分析
进入调试模式, 然后在for语句中单步调试, 并在printf ("%c", a[i]) 处设置断点, 可以看到, 输入sd的情况下, a数组的值为0x0018ff34, 然后在内存查看框中输入0x0018ff34, 会出现图3。
可以看到, 该数组从0x001834处开始存储, 第一个是73, 对应asc码的s, 第二个是64, 对应asc码的d, 第三个是0, 对应结尾的’ ’, 第四个就是上一节介绍的栈内填充字符0x CC, 后面一共17个0x CC, 加上前面三个, 正好20个元素。
这时候单步运行, 可以发现第一次循环, 输出了s, 第二次循环, 输出了d, 第三次循环, 输出了空格, 也就是’ ’, 第四次, 没有输出, 第五次, 输出了‘烫’字符, 于是可以看到, 第四次输出的是0x CC, 第五次输出的0x CC, 这两个0x CC一起组成了中文的‘烫’字符, 如图4所示。
至此, C语言程序数组越界所出现的‘烫’字符的原因已经揭示完毕, 就是因为编译器会在数组中填充16进制的0x CC, 进行越界访问, 则会输出中文的‘烫’字符。
5 结语
软件行业诞生以来, 各种形式的IDE, 高级语言, 类库, 框架, 开发模式如雨后春笋般迅速发展, 中国软件行业迅猛发展的时候, 却让很多人忽略了底层的重要性, 一直受制于人, 跟随着别人的类库做研发, 很难形成本国的核心竞争力。而诚如著名C++技术专家侯捷先生所说一样, “时下的很多IDE很多都是优秀的, 拜其所赐, 职场上的程序员多出几十倍, 而有多少人了解其内部机制呢”。
逆向技术在教育学术界一直处于比较尴尬的位置, 没有引起足够重视, 如果可以更多地关注通过逆向分析国外优秀软件的实现机制, 相信我国的软件行业必能取得长足进步。
参考文献
[1]Kip R.Irvine.Assemble Language for Intel-Based Computers[M].温玉杰等译.北京:电子工业出版社, 2007.
[2]Bjame Stroustrup.The C++Program ming Language[M].裘宗燕译.北京:机械工业出版社, 2010.
[3]杨季文.80x86汇编语言程序设计教程[M].北京:清华出版社, 2001.
[4]段钢.加密与解密[M].3版.北京:电子工业出版社, 2008.
[5]钱松林, 赵海旭.C++反汇编与逆向分析技术揭秘[M].北京:机械工业出版社, 2011.
[6]张一驰.软件恶意行为识别及其恶意性判定研究[D].郑州:解放军信息工程大学, 2012.
[7]陈福安, 刘宗田, 李力.8086 C语言反编译系统的设计及实现技术[J].小型微型计算机系统, 1993, 14 (4) .
[8]尚涛, 谷大武.软件防反汇编技术研究[J].计算机应用研究, 2009, 26 (12) .
[9]祁春霞.利用反汇编手段解析C语言函数[J].电脑与信息技术, 2011, 19 (4) .
写一个函数,用于比较两个字符串的比较(string_compare).
程序分析:
(1)主要思想:传入两个字符串后,比较这两个字符串中的每个元素,如果第一次比较就不相等,就不要让它进入到下面的比较中,这样一来,将它返回一个相减的值(即:两数组中开始不相等的那两个元素相减,返回值(int类型),是ASCII码值相减)。进入比较的过程中时,相等就返回0;其他情况都返回那个相减的值。
(2)主要方式:定义指针数组,并对其初始化。然后照上面的思想,进行代码的实现。
代码如下:
using namespace std;
void main(void)
{
if(“test”==“test”)
{
cout<<“相等”;
}
else
{
cout<<“不相等”;
}
}
上面的代码我们测试两个内容为test的字符串常量是否相等,按照常理,应该是相等的,这些在一些过程式语言中会得到相等的结论,但在c/c++却不是这样,
为什么呢?
答案在这里:因为字符串常量存储在计算机内存中,两个字符串常量的地址均不相同,所以这样的比较自然就不会得到我们所需要的结果,如果要进行是否相等的比较应该使用strcmp()这个涵数进行比较!
#include
#include
using namespace std;
void main(void)
{
if(strcmp(“test”,“test”)==0)
{
cout<<“相等”;
}
else
{
cout<<“不相等”;
}
cin.get();
}
strcmp()的函数原形是,int strcmp(const char* str1,const char* str)
相当将会返回一个等于0的整数,不相等的时候将会返回一个非0整数!
#include
#include
using namespace std;
void main(void)
{
char test[]=“test str!”;
char str[15];
strcpy(str,test);
cout<
int a[]={1,2,3,4,5};
int b[5];
memcpy(b,a,sizeof(a)*sizeof(int));
for(int i=0;i<5;i++)
{
cout<
}
cin.get();
}
//程序作者:管宁
//站点:www.cndev-lab.com
//所有稿件均有版权,如要转载,请务必著名出处和作者
上面的代码中的strcpy用来处理字符串数学组的copy,由于字符串数组属于const char*也就是常量指针所以是不能用a=“test str!”;的方式赋值的,接在后面的memcpy用于处理非 结尾的数组的copy处理,memcpy第三个参数是设置b在内存中所需要的内存空间大小所以用sizeof(a)*sizeof(int)来处理!
★ 例举线段相等的证明方法
★ 教案格式模板
★ 教案大全
★ 教案范本
★ 教案格式模板范文
★ 小学生教案
★ 初中生物经典教案
★ 家教案
★ 学生教案
这篇文章主要介绍了Go语言按字节截取字符串的方法,涉及Go语言操作字符串的技巧,非常具有实用价值,需要的朋友可以参考下
本文实例讲述了Go语言按字节截取字符串的方法,分享给大家供大家参考。具体实现方法如下:
代码如下:
// 按字节截取字符串 utf-8不乱码
func SubstrByByte(str string, length int) string {
bs := []byte(str)[:length]
bl := 0
for i:=len(bs)-1; i>=0; i-- {
switch {
case bs[i] >= 0 && bs[i] <= 127:
return string(bs[:i+1])
case bs[i] >= 128 && bs[i] <= 191:
bl++;
case bs[i] >= 192 && bs[i] <= 253:
cl := 0
switch {
case bs[i] & 252 == 252:
cl = 6
case bs[i] & 248 == 248:
cl = 5
case bs[i] & 240 == 240:
cl = 4
case bs[i] & 224 == 224:
cl = 3
default:
cl = 2
}
if bl+1 == cl {
return string(bs[:i+cl])
}
return string(bs[:i])
}
}
return “”
}