| Yangchun Luo 的个人资料Ace of the ACES照片日志列表 | 帮助 |
|
|
11月14日 Blogger 体验用特殊技术手段突破了中国政府对Google Blogger的访问限制。 http://blog.cls8.org/sonic1984乃我的新家,主要贴些无关痛痒的琐事和感悟。Google Blogger最爽之处除了简洁清新的界面风格,更在于它开放的精神。用户选择一个模版,然后就可以任意更改整个网站的源代码,绝对是高级用户的首选!See www.blogger.com for more details. Meanwhile,技术性的“天书”改发到http://blog.csdn.net/sonic1984去了。喜欢探讨技术的朋友们可以去那里交流。 至于现在这个MSN Space,暂时还没有想好它的定位。不过希望这种分离的方式能带来更specific的blog体验。 10月31日 最大的优点和最大的缺点晚上自习的时候突然想到一个问题:如果今后面试的时候被问到阐述一下自己的最大优点,我该怎么回答呢? 从小到大我觉得自己最引以为豪的地方就是自己能静得下心、坐得住。一本书、一支笔、一个人的状态是最惬意不过的了。每次上完自习,独走在夜晚的冷风中,那种感觉总是很畅快的。 但这也许是我最大的不足。喜欢安静、害怕别人打扰的性格有些时候会显得跟人群格格不入。跟人打交道也许是我最不擅场的了。有时看见别人在群体中八面玲珑游刃有余,虽然也会羡慕,但这绝不是我的风格。 最喜欢的状态是什么?人不在多,知心则行。四五个要好的朋友能在一起谈天说地、研究技术、切磋游戏,没有勾心斗角繁文缛节世间纷扰,这比什么都强! 10月29日 Introduction to C/C++ Binary LibraryPart 1. 需求分析:为什么要使用二进制库?
答案在于两个方面: 1) 隐藏源代码的需要。 放眼现在市面上各个中间件软件提供商,所提供的产品绝大多数都是以二进制库的形式发布的。流行的比如Linux下的Qt图形库,Windows下微软的DirectX图形接口、.NET基类,以及嵌入式平台上的MiniGUI等等。在Linux下静态库和动态库文件的扩展名通常为.lib和.so(Shared Object),而Windows下动态库也称作动态链接库,扩展名为.dll(Dynamic Link Library)。 2) 模块化、层次化设计的需要。 一旦某个软件模块设计完成,可以将其打包成一个独立的库文件。这样系统其他部分就可以按照其提供的接口进行调用,而无须考虑代码层次的兼容性。其次,库文件是已经编译好的二进制文件,被调用的库只需要静态或动态地链接到主调代码中即可,而无需再次编译库的源代码,大大提高了编译的速度和效率。 下面在Linux环境中使用流行的gcc编译器来演示这两种库的建立和使用。
Part 2. 案例1:C与动态共享库
假设有一个简单的任务(经典的helloworld例子) 有一个sayHello的方法在sayhello.c中定义: //file: sayhello.c #include<stdio.h> void sayHello() { printf("hello!\n"); } 头文件sayhello.h声明函数sayHello()的形式: //file: sayhello.h void sayHello(); 现在我们希望发布这个sayHello函数的实现,但是不希望别人知道源代码。于是我们可以将sayHello()函数打包成二进制库的形式发布,同时提供一个头文件声明其调用格式。 主程序hello.c调用sayHello(),如下: //file: hello.c #include"hello.h" int main() { sayHello(); return 0; } 生成动态库的过程如下: [sonic@Rex dynamic-lib-gcc]$ gcc -c -fpic sayhello.c 这样就生成了一个sayhello.o的目标文件。 参数-c: 编译或汇编源文件,但是不作连接.编译器输出对应于源文件的目标文件. 参数-fpic: Generate position-independent code (PIC) suitable for use in a shared library, if supported for the target machine. [sonic@Rex dynamic-lib-gcc]$ gcc -shared sayhello.o -o libHello.so 共享库libHello.so就生成好了。
现在假设第三方客户需要使用我们提供的共享库,只需要提供libHello.so和hello.h就行了。(hello.h包含于主程序hello.c中) [sonic@Rex dynamic-lib-gcc]$ gcc hello.c sayhello.so -o hello 把主程序文件和打包好的共享库文件一起编译,最后得到的文件是 [sonic@Rex dynamic-lib-gcc]$ ls hello hello.c libHello.so sayhello.h 但是这样还不能运行: [sonic@Rex dynamic-lib-gcc]$ ./hello ./hello: error while loading shared libraries: libHello.so: cannot open shared object file: No such file or directory 这是因为编译hello时用到了共享库,而运行的时候系统只在几个固定的目录进行查找: /lib、/usr/lib,其余的在配置文件/etc/ld.so.conf中指定。 这里有两种选择,一是把libHello.so拷入系统指定的目录,一是自己建一个专门放置共享库的目录。 比如/lib/local/shared-lib就是一个不错的选择。 修改/etc/ld.so.conf,将目录加入其中。 把libHello.so复制到/lib/local/shared-lib,然后以root账户运行/sbin/ldconfig更新系统缓存。 Ok,现在运行: [sonic@Rex dynamic-lib-gcc]$ ./hello hello! Bingo!试验成功! 用ldd检查一下编译完成的hello可执行文件: [sonic@Rex dynamic-lib-gcc]$ /usr/bin/ldd[1] hello libHello.so=> /usr/local/shared-lib/libHello.so (0x00a26000) libc.so.6 => /lib/tls/libc.so.6 (0x001d0000) /lib/ld-linux.so.2 (0x001b7000) 可以发现红色标记的那一行是我们提供的共享库。
Part 3. 案例2:C++与库
这是一个稍微复杂一点的例子。 比如我们要发布一个类,提供这样的头文件声明: //file: SaySomething.h class SaySomething { public: void sayhello(); void sayhi(); }; 成员函数的定义由两个文件承担: //file: SayHello.cpp #include "SaySomething.h" #include <iostream> using namespace std; void SaySomething::sayhello() { cout<<"hello"<<endl; }
//file: SayHi.cpp #include "SaySomething.h" #include <iostream> using namespace std; void SaySomething::sayhi() { cout<<"hi"<<endl; } 基于同样的理由,内部成员函数的具体实现是不能公开的,头文件是公开对外发布的唯一途径。
先以动态库形式编译: [sonic@Rex dynamic-lib-g++]$ g++ -c -fpic SayHello.cpp SayHi.cpp 生成了两个目标文件:SayHello.o和SayHi.o。 参数-c和-fpic与上例含义一致。 [sonic@Rex dynamic-lib-g++]$g++ -shared SayHello.o SayHi.o -o libSay.so 库文件libSay.so已经生成。 使用: //file: Main.cpp #include "SaySomething.h" int main() { SaySomething* sayHello=new SaySomething(); sayHello->sayhello(); sayHello->sayhi(); return 0; } 编译: [sonic@Rex dynamic-lib-g++]$ g++ Main.cpp libSay.so -o hello2 同样,将libSay.so放入之前建立的共享库目录/lib/local/shared-lib,运行: [sonic@Rex dynamic-lib-g++]$ ./hello2 hello hi 用ldd查看hello2: [sonic@Rex dynamic-lib-g++]$ /usr/bin/ldd hello2 libSay.so => /usr/local/shared-lib/libSay.so (0x0032b000) libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00a39000) libm.so.6 => /lib/tls/libm.so.6 (0x00301000) libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x0090f000) libc.so.6 => /lib/tls/libc.so.6 (0x001d0000) /lib/ld-linux.so.2 (0x001b7000) 结论是一致的。
还有一种方案是采用静态库的形式发布,当然,效果是一样的,达到了隐藏代码的目的。 [sonic@Rex static-lib-g++]$ g++ -c SayHello.cpp SayHi.cpp 这里没有使用-fpic选项,得到了两个同前缀的.o目标文件。 [sonic@Rex static-lib-g++]$ ar -r libSay.a SayHello.o SayHi.o 使用ar工具打包成.a文件(archive),编译: [sonic@Rex static-lib-g++]$ g++ Main.cpp libSay.a -o hello3 这次直接运行即可: [sonic@Rex static-lib-g++]$ ./hello3 hello hi 使用ldd查看的结果中也没有libSay的项了: [sonic@Rex static-lib-g++]$ /usr/bin/ldd hello3 libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00a39000) libm.so.6 => /lib/tls/libm.so.6 (0x00301000) libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x0090f000) libc.so.6 => /lib/tls/libc.so.6 (0x001d0000) /lib/ld-linux.so.2 (0x001b7000)
Part 4. 比较与分析
现在比较一下静态链接和动态链接。 静态链接下的各中间文件的最终可执行文件: [sonic@Rex static-lib-g++]$ ll -rw------- 1 sonic sonic 153 Oct 3 13:50 Main.cpp -rw------- 1 sonic sonic 133 Oct 3 13:50 SayHello.cpp -rw-rw-r-- 1 sonic sonic 2648 Oct 3 21:43 SayHello.o -rw------- 1 sonic sonic 128 Oct 3 13:50 SayHi.cpp -rw-rw-r-- 1 sonic sonic 2636 Oct 3 21:43 SayHi.o -rw------- 1 sonic sonic 71 Oct 3 13:50 SaySomething.h -rwxrwxr-x 1 sonic sonic 8230 Oct 3 21:47 hello3 -rw-rw-r-- 1 sonic sonic 5592 Oct 3 21:45 libSay.a 动态链接下的各中间文件的最终可执行文件: [sonic@Rex dynamic-lib-g++]$ ll -rw------- 1 sonic sonic 153 Oct 3 13:57 Main.cpp -rw------- 1 sonic sonic 133 Oct 3 13:57 SayHello.cpp -rw-rw-r-- 1 sonic sonic 3008 Oct 3 21:32 SayHello.o -rw------- 1 sonic sonic 128 Oct 3 13:57 SayHi.cpp -rw-rw-r-- 1 sonic sonic 3000 Oct 3 21:32 SayHi.o -rw------- 1 sonic sonic 71 Oct 3 13:57 SaySomething.h -rwxrwxr-x 1 sonic sonic 5669 Oct 3 21:37 hello2 -rwxrwxr-x 1 sonic sonic 7941 Oct 3 21:34 libSay.so
比较对应的文件发现,中间目标文件动态链接比静态略大,库文件动态链接明显大于静态,原因可能跟动态链接较静态链接更为复杂的机制有关。 最终可执行文件静态链接明显大于动态链接,原因很明显,动态链接顾名思义就是用到的时候才进行加载,因此主程序本身并不包含库。而静态链接就是把库在编译的时候就加载到可执行文件中,因此在运行时也不像同态链接那样需要库的支持。
这样一来,C/C++中头文件和库文件的关系就非常清楚了。 如下表所示。
Part 5. 一点疑问
动态链接产生的.so文件在和主程序文件一起编译的时候不能使用-static选项。如果强制使用的话,虽说可以通过链接,但是执行的时候会发生这样的错误: [sonic@Rex dynamic-lib-g++]$ g++ -static Main.cpp libSay.so [sonic@Rex dynamic-lib-g++]$ ./a.out -bash: ./a.out: /usr/lib/libc.so.1: bad ELF interpreter: No such file or directory 而静态链接产生的.a文件时可以和主程序文件一起用-static编译的。 [sonic@Rex static-lib-g++]$ g++ -static Main.cpp libHello.a [sonic@Rex static-lib-g++]$ ./a.out hello hi
10月16日 10T 归来涂完最后一道阅读题的答案,检查了一下时间,还有五分钟的剩余。再次把机读卡上和试卷上的答案对照了一下,没有发现错误。随后,监考老师停笔的信号发出,呈上试卷的那一刹那,总算长长地出了一口气——TOEFL,终于结束了!
总体来说,这次的感觉还是可以的。作文、听力、语法、阅读四个部分基本上都是正常水平发挥。按照平时模拟的成绩来算的话,也许能上640。如果真的能如愿以偿的话,暑假以来付出的努力也算没有付之东流了。 其实现在回想的话,还能清楚地记得当时考试的时候心理的压力还是蛮大的,特别是最开始的作文部分(后面详谈)。当时能稳住阵脚的一个非常重要的原因是有这么一个信念在支持自己——So far, so good. 随着完成部分的增加,这种信念愈来愈强烈。只要前面的题不出什么意外,到后面的时候压力就会越来越小,越来越轻松。否则,试想如果第一部分的作文写砸了,接下来哪里还有心情去认真做后面的题?。所以可以说,首当其冲的作文是4个section中最为重要的部分。
Part 2 Concerning the Composition (TWE)
30分钟构思一个给定话题然后写出一篇300多字的文章想必对大多数没经过准备的人来说都是一个挑战吧。而且即使经过充分的准备,话题的不确定性也使人心里没底。而且作文是第一场战役,其成败直接关系到后面3部分的发挥。 先说一下TOEFL作文的准备吧。 我是按新东方老师(David)提供的方法去进行准备的,市面上也有很多书介绍各种各样的技巧。其实,不管怎么样,自己动手写才是最为重要的,否则,一切方法都是空谈。David认为20篇是一个必要的指标。如果时间充足的话,当然是越多越好啦。但我只写了10篇左右就有点感觉了(一开始准备的时候作文是最头痛的部分)。第一篇差不多花了两个多小时,对照ETS的model essay才整出一篇稍微像样的文章。接下来按每天一到两篇的速度,到第11篇的时候基本上就能把纯粹的写作时间(不包括构思)压缩在30分钟左右。后来开学了就停了一段时间没写,到作模拟题的时候有写过两篇,30分钟的时间已经够了。 自己动手写的目的不光是训练写作的熟练度,还是为准备building blocks——考试时遇到类似的话题,把自己以前写的稍微改一改就用可以,省得到时候再想浪费时间。TOEFL作文基本上是从一个叫Official Pool of TWE topics的库中选取的,ETS的官方主页上可以下载ftp://ftp.ets.org/pub/toefl/989563wt.pdf . David强烈推荐必须要写得topic如下:5, 14, 15, 29, 48, 73, 110, 138, 152 基本上属于比较有代表性的题目。写完了这些还有兴趣的话就自己随机挑吧。考前建议把这185个话题都过一遍,列一下理由和重要的例子还有些相关单词就行了。网上都有每道题现成的理由,可以作为参考。这次作文的话题大意是 “What events make a child become an adult? Use specific reasons and examples to support your answer.” 之前我已经浏览过这个话题,大概记住了些理由,于是检了些便宜,哈哈。 再说下文章的格式和模板。 David推荐的格式是每段开头顶格写,两段中间空一行,这是现在欧美比较流行的风格。还有一种经典格式更汉语一样,也是可以接受的。 TOEFL作文一般是五段为宜。根据作文不同的类别各段有不同的安排。一般第一段都是题目的重述,同时表明自己的观点或倾向。 如果是对比性的文章,比如让你比较两种东西并做出一个选择,那么这两个方面一定都要写。反对的那个观点作为一段,少些点;支持的那个观点列出两个理由,分别在两段中进行阐述,篇幅肯定是要超过反对那个的,不然怎么突出你的论点。如果是单一话题的topic,比如这次这个,当然就列3个理由了占据三段了。 最后一段一般是总结3个理由段的主要内容,有能力的话可以好好写一下最后那句话,比如用点倒装什么的,说不定能收到奇效。 关于模版的内容非常多,只能阐述一个大概,更多内容请参阅新东方的课堂录音。 对于一个对比的话题,我喜欢的用的模版是这个样子的: Paragraph1: There is a heated discussion over the phenomenon that “the topic”. The advocators believe that “preference A”, whereas/while others who object to it argue that “preference B”. As far as I’m concerned, I’m for that former/latter preference. (Or you can say: Despite the fact that the major believe that “preference A” , I harbor a notion that “preference B”.) Paragraph2: There is no denying that “preference A” include some disadvantages. …… Paragraph3: However, the advantages to “preference B” far outweigh them. For a start/In the first place, “argument 1” Paragraph4: Meanwhile/In the second place/Moreover, “argument 2” Paragraph5: Taking into account of all discussed above, we can see that…
根据不同的地区监考的规则可能不尽相同。一般来说,绝对没有ETS bulletin上写得那么恐怖。 首先是跨区做题的问题,在哈尔滨考场,老师基本不会干涉你做题的过程。只要你不是提前把试卷拆开就Ok。至于你何时做哪个section,是不是在卷子上钩答案,完全是你自己的事,他们只监督中国意义上的作弊行为。另外,写作的题目是可以提前偷看的。卷子只是在边上封了一小块地方,完全可以在不破坏封条的情况下掀开表皮看都里面的内容,这样,构思的时间就省下来了。
暂时就写到这里吧。从国庆节以来复习的强度太大,以至于现在对TOEFL这个东西都有些厌恶了。考完的当天就把所有的复习资料“施舍”给周围的人了。成败在此一举,我也绝对不会再打算TOEFL Round 2了。 谨将此文献给现在奋勇备战ETS考试的,出于郁闷当中的和非郁闷状态当中的,认识的和不认识的或是即将认识的兄弟姐妹们,愿真主与你们同在,Amen. It’s your show time now. 10月9日 究竟TOEFL考什么?关键字:TOEFL 今天是十一复习行动的最后一天,离10月15日的最后检阅还有一个星期。从八月份上新东方集中狂补英语到十一期间大量做题,感觉知识层面的提升已经接近极限了。余下能影响考试成绩的,我感觉就是一个考试心态的问题了。
今天对这个问题特别有感触。上午作了一套2004年10月的真题,战况可谓惨不忍睹:每个section的错题数几乎都创了历史新高。其实在做题的过程中我差不多也预料到这个结果了。除去一些客观因素,比如听力录音被刻意加快了速度,试卷上的一些恼人的小错误等等,造成这次“TOEFL滑铁卢”主要还是当时的心态问题。这在阅读的时候感觉特别明显(btw, 10 errors in the reading comprehension section)。摸考时的气氛还依稀留在脑海中。从第一篇文章开始,我脑袋里就出现一个念头:一定要尽快作完!非常之奇怪,我也不知道当时为什么要怎么想,大概是怕时间不够吧。俗话说:“欲速则不达”,用在TOEFL考试中是最适合不过了。TOEFL阅读的文章本来就是在跟应试者玩文字游戏,其目的不是让读者尽量读懂而是根据其难度设计的需要走相反的方向。这就让一开始就想加快速度的我吃尽了苦头——越想快,越发现读不懂,就越急躁,本来能看懂的部分不知其所以然,能够运用解题技巧的地方也没有头绪了。
急躁这个东西是最影响发挥的,说起来人人都知道,人人都明白其危害,但是正所谓当局者迷,置身于那种环境当中,实在是很难把心态调整回来的。当时在读前两篇文章的时候,我也在反复告诫自己——“不能急,慢慢来”,但是眼看时间一分一秒在流逝,而自己却读了一大堆东西不知其所以然。时间已经花了却没有应有的结果。做题的时候就更郁闷了:原来状态好的时候看完文章直接就能选答案,根本不用回去确认,而且是那种板上钉钉毫无悬念的感觉;现在发现居然到什么地方去找对应都一头雾水,直接看选项呢,似乎至少有两个都有可能正确。怎么办,从头再读一遍似乎是不太可能了,只有寄希望在文章中随便看看碰碰运气看能不能恰好找到点对应,这样做题的结果就可想而知了。而且这种郁闷还会从一开始持续到最后,因为在后面文章的时候就会想,完了,这次栽定了,前面都可能错那么多,后面再怎么样都无济于事了。
下午对完答案,自然好做一番检讨,好好地回顾一遍上的战场。非常之奇怪twice,原来这些文章并不难懂啊,一字一句读下来,即使错最多那篇也懂了百分之七八十以上,做题绝对没有问题。这又让我想起了上午的情形:难道是真的读不懂吗?一个个单词似乎没有几个是不认识的,but一句话读完后居然在脑海中没有留下什么印象——well, that’s the point! 被焦虑、急躁填满的思考系统已经没有多余的空间来对文章进行分析了,眼前流过的只是些支离破碎的fragment,全然没有了文章的整体概念——连一句话的意思都要反复看几遍才基本弄懂,可知当时的阅读效率有多低了。还有一个地方,明明想到了是应用的分类逻辑,但已经没有心思去琢磨了。
分析到这个份上,当然是要找解决方案了。再说,这则日记的目的也不是给自己泄气的。显然,今天上午发生的情况在10月15日是绝对不允许再次出现的,否则,1k的报名费、花了这么多的时间岂不泡汤了(当然,也不能算是total loss,至少不管结果怎么样,自己英语能力的提高绝对是undeniable的)。所以,前两篇阅读的做题质量就成了关键。说归说,临场的发挥还得看当时的状态,现在只能不断告诫自己——不要刻意地去追求所谓的速度。相信每次TOEFL的难度都不会相差太多(否则便不能称之为TOEFL了),而且以前自己在阅读方面的战绩中的来说还是acceptable的,因此,如果按照正常的发挥,阅读取得理想的成绩是比较有把握的。那么就没有必要一定得催促自己刻意的加快速度。只要按照正常的节奏一步一步踏踏实实、按步就班的看下来,不让一些无谓的情绪充斥大脑,万事都Ok了!
花了三分之一个下午写这篇文章,感觉还是值得的。笑来同志说得对,题海战术是没有用的,唯有精做少量的题,仔细分析以前的错误,才能从跌倒的地方爬起来并保证今后不会再犯相同的错误——这才是做题的意义所在。
“为什么很多人准备托福永远不可能成功?他题海战术,做一套就扔掉,做一套就扔掉,从来不回顾,那你做来干麻嘛?得到虚假的欢乐。然后老的时候还会想,年轻的时候我也很努力。这就是他们的可悲之处。”——笑来原语
Ok,到此为止,谨以这篇文章来鼓励自己,希望在10月15日取得自己理想的成绩吧! Amen 9月4日 作为面试官得到的几点体会关键字:面试经验 俱乐部的面试工作总算接近尾声了,作为新成立的“嵌入式开发”小组的负责人,有幸参加了三次面试审评工作。以前都是被别人面,现在自己坐在考官的位置,角色的转换,使我了解到:原来面试绝对不是自己原来想象的那样! 是我感触最深的一点是,面试的时候被面试者往往过分强调自己加入一个组织能得到什么,但是作为考官的我,更多想到的却是你加入本俱乐部,能给我们带来些什么。遗憾的是,几乎没有面试者在这方面去考虑。这是我们在今后找工作、申请留学的时候都可以借鉴的。 其次是面试时被面试者表现出的态度和精神面貌也相当重要。你可以很厉害,但如果不懂礼貌,或则说过分自信、甚至盛气凌人,这样的人绝对是不受欢迎的。相反,即使实力上有所欠缺,但表现得十分诚恳,就算是几分腼腆,给人也会是一种孺子可教、有潜力可挖崛的感觉。这次有一个人从简历上来看的确有几分实力,但是一进办公室就问:“呀,这就是实验室啊?”“你们这里都干些什么啊?”给人的印象就像是上面的领导来指导工作的感觉,显然被拒就不可避免了。还有一个人,本来面试都差不多了,但是最后一句“你们什么时候通知我啊?要这样等下去得等到什么时候啊?”而留下了非常糟糕的印象,在最后的集体讨论中被一致否决。而一个非常腼腆的小伙子,尽管说话时非常的紧张,不停地搓着手,但我们可以从他的谈话中看出他的诚恳、他的真诚、他对技术的热爱,这样的同学,虽说基础不好,也不是计算机专业的,但我们还是把他留下了。 最后但绝非最不重要的(Last but not the least),简历一定要有针对性。与主题无关的东西不要在简历上写也不要在面试说。面试不是Conversation,考官希望在短短的几分钟之内知道你这个人是不是他所需要的。过多的其他信息往往喧宾夺主。今天最后一个小子,常常的简历看起来巨厉害无比:会C、C++、Pascal、Fortran、Delphi、C#、Java,在Linux下写过程序,连OS/2这样一个极不常见的操作系统都玩过。天啊,疯牛级的人物!当我们怀着将信将疑的心情进行面试的时候,才发现这些不过都是在夸夸其谈。我们总是试图问他一些非常细节的技术问题以探明其底细,但是每次他都不正面回答,而是十分巧妙的绕开了。我们的结论只能是这种人也许根本就不熟悉技术,只会用表面的东西来唬人。也许他换一种方式来写简历,只把自己最拿手的东西写上,我们面试的时候也好有针对性地提问,不至于被大量的旁枝末节而分散精力。 总之,这次招新活动虽然耽搁了不少时间,但是收获是巨大的。面试方想要从面试中得知其实非常明确:我们招的人是否符合提供的岗位。包括此人的人品是否诚实,是否能静下心从事所分配的任务,是否有责任感,是否能与其他队员和谐相处等等,反而技术层面的东西考虑得不是最多。 7月19日 Introduction to C# Inferface关键字:C# 接口 泛型编程 面向对象 C#和Java中接口(interface)都是一个十分重要的概念。 什么是接口?按照我的理解,每一个接口代表从一类数据结构对象中抽象出来的一组公共操作。比如接口IComparable,表示可比较操作,C#中绝大多数预定义的基本数据类型都支持该操作,相应的,它们都从IComparable中派生出来。又比如System.String类,它支持的操作有可比较、可枚举、可复制、可转换。于是MSDN文档中,C# String类的定义如下: public sealed class String : IComparable, ICloneable, IConvertible, IEnumerable 前面提到派生这个词,实际上,接口就是类的一个子集。接口中定义的函数隐式地被认为是抽象和公有的。这样,定义一个接口的时候,这需要提供成员函数的返回类型和参数列表(其余的不用也不能提供),其作用相当于一个占位符(但是和抽象基类中的abstract成员函数还是有所区别)。 由于接口的定义属于申明性质的,即没有提供任何的方法,于是实现接口的工作就交给了接口上层的具体数据接口来完成。这样的好处是显而易见的:首先,接口把这类数据结构的公共操作抽象出来,形成相同的函数接口,便于统一进行操作,这在后面的例子中会有充分的体现;其次,实现不同数据结构的统一操作可以大大简化算法的设计,不同内部存储形式的型别(顺序访问型、随机访问型、树型、网格型)只要按照某一接口的设计格式抽象成一致的函数,就能用同一个函数把它们在逻辑上相同的操作(比如遍历、比较等等)封装起来。Array和ArrayList是两个存储结构完全不同的类型,但在逻辑上确实相似的。用foreach函数对他们的遍历得到的结果都是一样的,这就要归公于接口这个伟大的设计概念。还有一个好处就是,接口只提供访问,不提供具体干什么。还是如上例,foreach返回的只是一个指向实际数据的引用,具体是要输出该数据还是修改该数据完全取决于调用者本身的意愿。(在后面的例子中细说)特别值得一提是,上面所说的一切“数据”都是一个泛泛的概念,并不涉及到底是哪一种数据。这大概就是所谓的泛型编程吧(Generic Programming)。 以前我在用C++编写数据结构算法时,不知道接口这个概念。同一种相同的访问需求由于具体操作(如输出到屏幕还是文件)的不同而要写成多个不同的函数,后来使用函数指针的概念使之得到了改进(即把进行某种操作的函数以函数指针的形式通过参数的形式传递给遍历函数。现在发现,通过接口这个概念进行的设进似乎更为优雅。 下面以二叉搜索树(Binary Search Tree)为例说明这种思想的优越性。 C#中已人为定义好一个二叉搜索树的类。该类现在能实现的操作是插入数据和中序遍历(输出的结果为升序序列)。在主函数中可以调用成员函数void inorderTraversal(void)来完成中序遍历并输出到屏幕的动作。现在看起来似乎没有实现接口的必要。但是如果接下来又想增加输出到文件、每个元素自增1的动作则让人十分头疼。为什么呢?具体的操作是包含在inorderTraversal中的,所谓的紧耦合(tight coupling)关系。要改变操作就得改变整个inorderTraversal,即使只是一行代码而已!当然可以用前面提到的函数指针来解决,但是那是在C++中,C#连指针都取消了,至于函数指针就我现在不到半个月的C#经历而言实在无从得知。所幸的是,接口为我们提供优雅的解决方案。只要能像Array/ArrayList一样实现IEnumerable接口,就能使用foreach控制语句简洁的完成遍历操作。 在主函数中我们的目标如下 BinaryTree bt=new BinaryTree(); foreach(TreeNode tn in bt) Console.Write(tn.ToString()+' '); //屏幕输出 foreach(TreeNode tn in bt) m_writer.WriteLine (tn.ToString()+' '); //写入文件 foreach(TreeNode tn in bt) do_some_other_things(tn.Value) //修改数据 要实现IEnumerable接口,查阅文档可以得到相关方法。 public class BinaryTree : IEnumerable //BinaryTree从IEnumerable派生 { ... public IEnumerator GetEnumerator() //实现IEnumerable定义的抽象函数 { m_seq.Clear(); sequence_number=0; this.inorderSeq(); //内部方法,以支持中根遍历的顺序 return new BTEnumerator(m_seq, sequence_number); //调用BTEnumerable类的构造函数 } } 根据文档的说明,实际操作遍历的其实是一个接口IEnumerator的派生类。IEnumerator也是一个接口,提供3个方法用以支持遍历。分别是Current(属性Property,返回当前的数据元素的引用)、MoveNext()(函数,把Current移动到下一个元素)、Reset()(重置到第一个元素的前一位置)。这里我把这个派生类命名为 BTEnumerable。它的实现是关键: class BTEnumerator : IEnumerator { ... public BTEnumerator(Hashtable hash, int max_number) { ... //构造函数 }
//提供基接口抽象函数的实现 public object Current { get { return m_hash[m_curr]; } }
public void Reset() { m_curr=-1; }
public bool MoveNext() { return ++m_curr<m_count; } } OK,这样就完全实现了IEnumerable接口,可以用foreach对BinaryTree进行任意合理操作了。 下面,我们还可以手工模拟foreach操作: IEnumerator iter=bt.GetEnumerator(); while(iter.MoveNext()) Console.Write(iter.Current.ToString()+','); 首先取得一个迭代器,然后利用该迭代器进行迭代操作。 7月2日 来自国外教授的回复关键字:缓冲区溢出 Linux保护机制 Execshield 打开慢腾腾的MSN邮箱,本来没有抱什么希望的我突然眼前一亮,”A New Message”,不会又是垃圾邮件来浪费我的表情吧?4、5seconds之后,蜗牛般的浏览器终于给出了答案。From: Randy Bryant Randy.Bryant@cs.cmu.edu。 3、4天前在《深入理解计算机系统》上遇到的一个家庭作业问题令我揪心了很久。我分析了各种情况,得出的结论是只有一种办法可以解决这个问题。但是在我实行的时候却意外地收到一个系统给出的”Segmentation fault”。反复检查了输入的字符串,确实没有问题。用gdb调试时发现错误发生在子函数返回的ret语句上。设置断点,检查各相关寄存器的值,一切都符合预期。为什么一到ret就挂呢?猛然间我想起了几年前看过的一片介绍性的文章,讲的是“缓冲区溢出的保护措施”。我又把返回地址改为程序段任意一个地址,这次ret通过了。原来这个“先进”的操作系统从底层就禁止在数据段/程序堆栈的地方运行代码,怪不得我ret到自己预设的地址(位于程序堆栈内)时会收到错误。 这个问题看来是弄清楚了,但是怎么解决那个家庭作业呢?想了很久还是没有答案。旁边又没有一个钻研这个东西的同志。能不能问问作者啊?我想。 以前从来没有给国外的陌生人发过邮件。屈指可数的几次经历是给Konami美国分部的网站写了一封信询问某游戏、Sony的网站询问能否下载sonicstage。好在对方都有回复,现在胆子也大了点。 问就问吧,也没什么大不了的。 To: randy.bryant@cs.cmu.edu Sent: Saturday, June 25, 2005 9:24 AM Subject: A question about one of CS:APP's homework
Hello Prof. Bryant,
I got a problem when I was reading Chapter 3 of CS:APP. That is homework 3.38. The description is as followed(copied from the book).
Homework Problem 3.38 [Category 3]: In this problem, you will mount a buffer overflow attack on your own program. As stated earlier, we do not condone using this or any other form of attack to gain unauthorized access to a system, but by doing this exercise, you will learn a lot about machine-level programming...
To solve this problem, I think the only way is to overwrite the return address of function getbuf(), make it point to a specified address where some executive codes is set from the input. I run the program under GDB. When I do this, I received a segmentation fault. Maybe the operation system prevents program running from the data segment or program stack. I wonder is there any other way to change the process of a program? What is the exact inputing string to make getbuf() return 559038737(0xdeadbeef) to test()? If you're too busy to reply, just give me the answer.
Linux Kernel is version 2.6.9-1.667, gcc version 3.4.2, gdb version 6.1
Thanks a lot. Your help is very essential to my understanding of this book.
Sonny Jun 26, 2005
国外的教授就是负责任啊。两天后我收到了一封长长的reply。 From : Randy Bryant <Randy.Bryant@cs.cmu.edu> Sent : Monday, June 27, 2005 10:08 PM Subject : RE: A question about one of CS:APP's homework
You'll notice that this problem is rated as a difficult problem. That's partly because it's hard to determine why a particular solution doesn't work. To solve this problem, you need to have an exploit string that:
1. Overwrites the return address to be that of your exploit code 2. Write exploit code that alters the return value, corrects the return address, and re-executes the return 3. Sets the saved value of %ebp to its original value.
If you do any of these steps wrong, you'll get a segmentation fault. You can used GDB to help you step through the code, but even this is difficult.
That said, it is possible that your Linux system has mechanisms that make the assignment much more difficult. Some versions of Linux 2.6 contain a technology called "ExecShield". This does the following: * Make it so that code on the stack is not executable * Use randomization, so that the stack locations change with each execution.
You can test whether your version is using randomization by running a program under GDB multiple times. Set a breakpoint in main, and each time check the value of %ebp. If it changes every time, then your OS is using randomization.
You can disable Execshield at the time you boot your system.
文中提到的ExecShield就是一直困扰我的保护机制。首先它使程序堆栈内的代码不可执行;其次,每次执行的时候都会随机地改变程序堆栈的地址,这样就使攻击者无法得知每次的堆栈地址而无法算出返回地址(当然可以用调试器gdb得到,但这样就没有使用价值,仅仅适合研究)。Bryant教授提到的3个必须点也都是我想到的:1. 覆盖函数的返回地址以使之指向你自己设置的代码;2. 在自己设置的代码中改写程序的行为(此处为改变返回值),更正返回地址,重新执行ret操作;3. 恢复原先的ebp寄存器的值。 虽然由于底层系统的高安全性没能完成这个作业,但是我学到的很多东西。更清晰地了解了汇编级代码的工作过程,尤其是函数的调用与返回;对缓冲区溢出这个概念有了一些深入的体会,当今一些网络攻击程序的基本攻击原理算是初步理解(尽管经过矛与盾的较量,攻击的手段变得越来越高超,但是其本质的东西是不会变的);第一次给国外的教授写信求教并且得到了非常详尽的解答,心中还是非常开心的,呵呵呵呵。 6月25日 缓冲区溢出浅析关键字:汇编,缓冲区溢出 Part 1 书评 最近在看得一本书是《深入理解计算机系统》,英文原名是Computer System: A Programmer’s Perspective。也是一个偶然的机会才在别人的书桌上随便翻开看看的。结果一发不可收拾,现在自己花了RMB72购入囊中,列入珍藏的书目中了。正如英文的原名所叙述的,from a programmer’s perspective, 故名思义,就是从程序员的视角来看待一个计算机系统。现有的一些计算机原理书,往往过于偏硬,什么门电路啊、译码器啊讲的非常深入,但与现实就相距甚远,常常让人理解起来没有参考。这部书,我刚看完第三章,从页码来看差不多有1/4多了。现在最大的感触就是书上讲的东西下来都可以自己在机器上实践(特别是第三章“程序的机器级表示”),把汇编语言与高级的C语言有机地结合起来学习,通过熟悉的C语言的知识去影射难以理解汇编代码,我觉得是这章最成功的最出彩之处。不像有的汇编书一上来就是一大堆指令让人记忆,这部书把C的代码/控制结构直接汇编成汇编语言的形式,用多少指令才讲多少,步步为营。看完前三章,我最大的收获是对程序的理解有上了一个新的高度。以前只是局限于高级语言的代码,各种语言(比如我学过的QB、Pascal、C、C++)都是分散的模块。现在站在机器级代码的角度,各种语言的殊途同归,大家本事同根生嘛!!
Part 2 关于缓冲区溢出攻击 还有一大收获是对程序堆栈的理解。在子程序中定义的局部变量都是放在程序堆栈中的,而子程序/函数的返回地址也是放在程序堆栈中。返回地址是什么?就是直接传递给指令指针(Instruction Pointer,IP)的指。如果改变了IP,也就相当于改变了程序的流向!(想到了什么?对,这就是恶意程序!)那么怎样才能去改变IP呢?C中指针的概念。我们在子程序中申请一个字符串,系统就会在程序堆栈中安排一个缓冲区(buffer)来储存。这个缓冲区当然是有限的。如果我们输入的字符串内容(假设程序与用户交互会要求输入的是字符串)超过了缓冲区的长度(称为缓冲区溢出),那么输入的内容就会覆盖程序堆栈中之前保存的内容。这,就给我们改写返回地址创造了条件!!记住,机器级的程序可没有人像的那么聪明。早期的恶意攻击程序大概是这样子的:在提示输入数据时(字符串),利用有效的空间输入攻击者想执行的机器代码(16进制表示的,基本看不懂,得用其他程序来生成),然后利用缓冲区溢出来改写子程序的返回地址,把这个地址指向刚才输入的机器代码的开始位置。这样当该子程序返回时,正常的程序流程就被破坏了,转而去执行攻击者预先设定的代码(可以格式化磁盘,可以在远程主机上留下后门等等,自己想)。接下来,这台“肉鸡”就任你宰割了,哈哈!当然这些是早期的攻击办法,现在的系统(软件/硬件)都能实现让指令只在代码段执行。这样在程序堆栈段输入的代码,即使强制是IP跳转到那里,也不能执行,因为位于代码段之外的指令时非法的。我打算进行的一个缓冲区溢出试验就遇到了这个问题。如果把返回地址改为代码段的随便一个地址,可以继续执行下去,其实程序根本不知道它的流程已经被我改掉了;但是设为堆栈段的地址,在ret(汇编指令,返回前一个函数之意)时有收到一个段错误(segmentation fault),原因前面已经叙述过了。看来,补丁太多了也不完全是件好事啊!呵呵。 |
|||||||||||||||||||||||||||||||
|
|