显示标签为“计算机软件-底层”的博文。显示所有博文
显示标签为“计算机软件-底层”的博文。显示所有博文

2007年6月5日星期二

为什么Win32下运行DOS程序资源占用率这么高?

author:nforcex

Q:为什么PC上运行Turbo C 2.0系统占用率如此之高?系统变得缓慢?
A:在采用 x86-32(Intel & AMD)处理器的PC上,CPU是通过虚拟机的技术来模拟8086的架构的--MS-DOS是运行于8086之上的,当DOS下进程运行时,模拟的DOS 通过陷阱使I/O等操作陷入Windows核心态中,配合CPU的部分硬件虚拟,来虚拟8086内核,因为虚拟过程中使用了不计其数的中断等操作(陷阱和虚拟的缘故),使得系统效率奇低无比。

所以当你在AMD Athlon64 X2 4800+上或者是P4 XEE上运行小小的DOS程序(记住,不是控制台程序)仍然奇慢无比或者系统资源占用很高的时候不要摸不着头脑或者抱怨什么了,只不过是虚拟的8086效率太低导致的罢了。

Windows和Unix的线程_摘抄

这里先介绍下线程的概念,线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,Sun的Solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程,不能像Windows NT系统那样(2000是NT5.0,XP是NT5.1,2K3是NT5.2)一个进程有很多个线程。现在,多线程技术已经被许多操作系统所支持,包括 Windows/NT和Linux。但是线程的实现在类Unix下并不相同,基本上分为内核支持方式和用户空间支持方式,如果线程的上下文切换是在内核中实现的,我们就称之为内核方式实现,但如果线程的切换是在用户空间进行的我们就称之为用户方式实现,内核并不知情,当然还有两种方式的混合方式,用户空间中的多个线程在内核空间有相应的内核线程与之对应(通常我们称此内核线程为LWP-轻级进程)。
我们再看看linux下线程的实现,linux的线程编程有两个库pthread和pth,对于pthread的实现是内核方式的实现,每个线程在 kernel中都有task结构与之对应,也就是说用ps命令行是可以看见多个线程,线程的调度也是由内核中的schedule进行的。
再来看看Windows的多线程,Windows NT和Windows95是一个抢先型多任务、多线程操作系统。因为它使用抢先型的多任务,所以它拥有与UNIX同样平滑的处理和进程独立。多线程就更进一步。一个独立的程序默认是使用一个线程,不过它可以将自己分解为几个独立的线程来执行,例如,其中的一个线程可以发送一个文件到打印机,而另一个可以响应用户的输入。这个简单的程序设计修改可以明显减少用户等待的时间,让用户无需担心长时间的计算、重绘屏幕、文件读写等带来的不便。
多线程还可以让你从许多高端的多处理器NT机器中得到好处。例如,你购买了一个高级的RISC机器,可以使用多达10个CPU芯片,但在开始的时候你只购买了一个CPU。你写了一个简单的Mandelbrot set程序,你发现需要15秒的时间来重新绘制Mandelbrot set的画面。

那么,Windows平台的线程和类Unix平台(包括Linux)的进程的区别是什么呢?
  熟悉WIN32编程的人一定知道,WIN32的进程管理方式与UNIX上有着很大区别,在UNIX里,只有进程的概念,但在WIN32里却还有一个“线程”的概念,那么UNIX和WIN32在这里究竟有着什么区别呢?
  UNIX里的fork是七十年代UNIX早期的开发者经过长期在理论和实践上的艰苦探索后取得的成果,一方面,它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简洁明了的多进程方法。
  WIN32里的进程/线程是继承自OS/2的。在WIN32里,“进程”是指一个程序,而“线程”是一个“进程”里的一个执行“线索”。从核心上讲,WIN32的多进程与UNIX并无多大的区别,在WIN32里的线程才相当于UNIX的进程,是一个实际正在执行的代码。但是,WIN32里同一个进程里各个线程之间是共享数据段的。这才是与UNIX的进程最大的不同。
对于多任务系统,共享数据区是必要的,但也是一个容易引起混乱的问题,在WIN32下,一个程序员很容易忘记线程之间的数据是共享的这一情况,一个线程修改过一个变量后,另一个线程却又修改了它,结果引起程序出问题。但在UNIX下,由于变量本来并不共享,而由程序员来显式地指定要共享的数据,使程序变得更清晰与安全。
至于WIN32的“进程”概念,其含义则是“应用程序”,也就是相当于UNIX下的exec了。

2007年5月1日星期二

新书试读

《Rootkits——Windows内核的安全防护》
最近最让IT管理员头痛的是什么呢?--毫无疑问是rootkit。这种可恶的程序是一批工具集,黑客用它来掩饰对计算机网络的入侵并获得管理员访问权限。一旦黑客获得管理员访问权限,就会利用已知的漏洞或者破解密码来安装rootkit。然后rootkit会收集网络上的用户ID和密码,这样黑客就具有高级访问权限了。


《超越C++标准库:Boost库导论》
谁说ANSI/ISO C++会陷入无穷无尽的底层机关?谁说只有JAVA才有高效的垃圾清理机制?看看C++的开源项目Boost吧,它为我们创造好了优秀的轮子。

2007年4月25日星期三

内存错误-原理角度·A Programmer's Perspective_转载

-----------------作者: 林锐 博士 (稍有整合改动)
对于程序员来说内存是一片危机四伏的沼泽(当然这在JAVA和CLR程序中已经大为改观)。
比尔·盖茨也失言了:640K ought to be enough for everybody。当前内存连Desktop也普遍标配1GB的容量。不仅容量,内存管理的复杂度也在提升,只不过随着软件的进化,不断地分层,有些问题对于不同层面的开发人员透明了。
程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。
局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

常见的内存错误及其对策
发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。
常见的内存错误及其对策如下:
1.内存分配未成功,却使用了它。
编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

2.内存分配虽然成功,但是尚未初始化就引用它。
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

3.内存分配成功并且已经初始化,但操作越过了内存的边界。
例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

4.忘记了释放内存,造成内存泄露。
含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。
动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。

5.释放了内存却继续使用它。
有三种情况:
(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
(2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
(3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

Summary,我们要注意:
1.用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存;
2.不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用;
3.避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作;
4.动态内存的申请与释放必须配对,防止内存泄漏;
5.用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

下面举几个经典的错误例子,大家不要犯同样的错误:
1. 返回栈内存指针
char *GetString(void)
{
char *p = "hello world";
return p;
}
char* pGet = GetString();

这段程序编译时没有错误,运行也没有错误,但是你却无法使得返回的pGet指针指向的数据是你想要的“hello world”,因为指针p的生命期是函数GetString内,运行完函数GetString后,p分配的栈空间马上被系统回收了。虽然pGet指向了p当初分配的内存地址,但是那块地址已经没有内容了。

2.这是一个出现频率非常高的错误
char* pChar = new char;
……
int a ;
pChar = &a;
……
delete pChar;
当然这是一个例子,具体的程序各有不同。
这段程序有两个问题。一是pChar = &a;将导致pChar原先分配的空间无法再被获取,就象我们的丢失了朋友的电话号码一样,无法再联系这个朋友了。这就造成了内存泄漏。如果内存泄漏多了,可能导致系统的崩溃,因为可用的资源将越来越少,直到枯竭为止。第二个问题是delete pChar将导致异常发生,因为这时的pChar已经不是指向动态分配的内存了,而是指向了a分配的栈空间,而栈空间是不能使用delete来回收的,因此将导致内存异常。

内存错误-应用角度·A Programmer's Perspective_整理资料

使用Windows操作系统的人有时会遇到这样的错误信息:“0X????????指令引用的0x00000000内存,该内存不能written”,然后应用程序被关闭。如果去请教一些“高手”,得到的回答往往是“Windows就是这样不稳定”之类的义愤和不屑。其实,这个错误并不一定是Windows不稳定造成的。下面我们就来简单分析这种错误的常见原因。

  一、应用程序没有检查内存分配失败
  程序需要一块内存用以保存数据时,就需要调用操作系统提供的“功能函数”来申请,如果内存分配成功,函数就会将所新开辟的内存区地址返回给应用程序,应用程序就可以通过这个地址使用这块内存。这就是“动态内存分配”,内存地址也就是编程中的“指针”。
  内存不是永远都招之即来、用之不尽的,有时候内存分配也会失败。当分配失败时系统函数会返回一个0值,这时返回值“0”已不表示新启用的指针,而是系统向应用程序发出的一个通知,告知出现了错误。作为应用程序,在每一次申请内存后都应该检查返回值是否为0,如果是,则意味着出现了故障,应该采取一些措施挽救,这就增强了程序的“健壮性”。
  若应用程序没有检查这个错误,它就会按照“思维惯性”认为这个值是给它分配的可用指针,继续在之后的运行中使用这块内存。真正的0地址内存区保存的是计算机系统中最重要的“中断描述符表”,绝对不允许应用程序使用。在没有保护机制的操作系统下(如DOS),写数据到这个地址会导致立即死机,而在健壮的操作系统中,如Windows等,这个操作会马上被系统的保护机制捕获,其结果就是由操作系统强行关闭出错的应用程序,以防止其错误扩大。这时候,就会出现上述的“写内存”错误,并指出被引用的内存地址为“0x00000000”。
  内存分配失败故障的原因很多,内存不够、系统函数的版本不匹配等都可能有影响。因此,这种分配失败多见于操作系统使用很长时间后,安装了多种应用程序(包括无意中“安装”的病毒程序),更改了大量的系统参数和系统文件之后。

  二、应用程序由于自身BUG引用了不正常的内存指针
  在使用动态分配的应用程序中,有时会有这样的情况出现:程序试图读写一块“应该可用”的内存,但不知为什么,这个预料中可用的指针已经失效了。有可能是“忘记了”向操作系统要求分配,也可能是程序自己在某个时候已经注销了这块内存而“没有留意”等等。注销了的内存被系统回收,其访问权已经不属于该应用程序,因此读写操作也同样会触发系统的保护机制,企图“违法”的程序唯一的下场就是被操作终止运行,回收全部资源。计算机世界的法律还是要比人类有效和严厉得多啊!
  像这样的情况都属于程序自身的BUG,你往往可在特定的操作顺序下重现错误。无效指针不一定总是0,因此错误提示中的内存地址也不一定为“0x00000000”,而是其他随机数字。
Q:运行Win32平台下程序的时候出现了如下问题,这是怎么回事?

A:那是因为,程序由于自身的bug引用了不正常的内存指针(至少从VC MFC编译的可执行体来说是这样子的,其他可以类比)。在使用动态内存分配的应用程序中,有时候程序会因为bug或者和OS互动的偶然性错误出现读写本来可以使用但是“因为特殊原因失效”的内存,这个时候,触发Windows内核的保护机制,“违法”操作的程序被终止运行,回收系统资源。如果是0X00000000一般来说是指到了无效零指针。如果是0X77f8206b,那可能是因为程序的bug导致该片内存区域失效。可以通过一些反向工程工具查看分析错误的位置。

VC MFC之“stdafx.h”释疑

author:nforcex
所谓头文件预编译,就是把一个工程(Project)中使用的一些MFC标准头文件(如Windows.H、Afxwin.H)预先编译,以后该工程编译时,不再编译这部分头文件,仅仅使用预编译的结果。这样可以加快编译速度,节省时间。预编译头文件通过编译stdafx.cpp生成,以工程名命名,由于预编译的头文件的后缀是“pch”,所以编译结果文件是projectname.pch。
编译器通过一个头文件stdafx.h来使用预编译头文件。stdafx.h这个头文件名是可以在project的编译设置里指定的。编译器认为,所有在指令#include "stdafx.h"前的代码都是预编译的,它跳过#include "stdafx. h"指令,使用projectname.pch编译这条指令之后的所有代码。因此,所有的CPP实现文件第一条语句都是:#include "stdafx.h"。
换句话说:Stdafx.cpp是一个只有一条语句(#include "stdafx.h")的空文件;Stdafx.h是Visual C++为每个项目配置的用来预编译的文件,在Stdafx.h文件中可以加入应用程序所需的头文件;那有人会问afx是什么意思? afx是Application Framework 的缩写,afx_ 泛指MFC中的全局变量和全局函数,af 就是 application framework,x添进去为了凑出三个字母吧。

2007年3月27日星期二

字符集的发展--摘抄

1. 编码问题的由来,相关概念的理解1.1 字符与编码的发展从计算机对多国语言的支持角度看,大致可以分为三个阶段:
阶段一:
系统内码 ASCII
说明 计算机刚开始只支持英语,其它语言不能够在计算机上存储和显示。
系统 英文 DOS

阶段二:
系统内码 ANSI编码(本地化)。
说明 为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 \'中\' 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
系统 中文 DOS,中文 Windows 95/98,日文 Windows 95/98

阶段三:
系统内码 UNICODE(国际化)
说明 为了使国际间信息交流更加方便,国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。
系统 Windows NT/2000/XP,Linux,Java

2. 字符,字节,字符串理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:

字符
人们使用的记号,抽象意义上的一个符号。
\'1\', \'中\', \'a\', \'$\', \'¥\', ……


字节
计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。
0x01, 0x45, 0xFA, ……


ANSI字符串
在内存中,如果“字符”是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为 ANSI 字符串或者多字节字符串。
中文123(占7字节)


UNICODE字符串
在内存中,如果“字符”是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串。
L中文123(占10字节)