# 二十六、漏洞抽象定律 你每天不可或缺的Internet里有个关键的小魔法,这个魔法就在TCP通讯协议这 个internet的基础协定里。 TCP是一种可靠的数据传输方法。我说可靠是指如果用TCP在网络上传一个讯息, 讯息一定会到,绝不会乱掉或坏掉。 TCP的用途很多,比如抓取网页数据或传电子邮件都是。由于TCP这么可靠,连那 些挪用钱的东非人电邮(译注:指有阵子常见到的骗人信)都能完整无缺的到达, 真是好笑。 相对的有另一种叫IP的不_数据传输方法。丨P不保证数据会传到,就算到了数 据也可能会乱掉。如果你用IP传送一堆讯息,很可能只有一半的讯息到达,而 且其中还有一些到达的顺序和原先传送时的顺序不同,另外可能有几个讯息的内 容会变掉,可能变成可爱的猩猩宝贝照片,更可能变成一堆看不懂的垃圾,看 起来就像台湾垃圾信的标题一样。 这里就是魔法所在:TCP是架在IP上面的。换句话说,TCP不得不靠一个不可靠游 工為想办法可靠地传送数据。 为了说明这的确是个魔法,想想下面这个本质上相同(虽然有点滑稽),来自真实 世界的情节。 想象你有个方法把演员由百老汇送到好莱坞,基本上就是让人坐上车后开车横越 国家送过去。有些车会出车祸让可怜的演员挂掉。有时候演员在路上喝醉了就 去剃光头或刺纳粹刺青,结果变得太丑而不能在好莱坞工作。另外由于走的路线 不同,演员到达的顺序常会跟出发的顺序不一样。现在想象有个叫好莱坞快递 的新服务,可以把演员送到好莱坞,并且保证演员一定会(a)到达,并保证(b) 顺序不变而且(c)状态完美地到达。神奇之处在于好莱坞快递除了原本的车子以 外,并没有新的运送方法。好莱坞快递的作法是在每个演员抵达时检查演员的状 况,如果状况不佳就打电话请公司把该演员的双胞胎送来。如果演员到达的顺序 不对,好莱坞快递会照正确顺序重新排好。如果51区有架大幽浮在内华达的高速 公路上坠毁阻断了交通,预定走这条路线的演员就会改走亚历桑那州,好莱坞快 递甚至不会把事情告诉加州的导演。导演只会觉得演员来得比平常慢,他们甚 至不会所至幽浮失事的消息。 TCP的魔法大致上就是这样。这种作法常被计算机科学家称为猶康:把复杂许多 的东西隐藏起来的一种简化动作。结果很多计算机程序的设计都是在建立抽象 机制。字符串链接库是什么?它是一种伪装,假装计算机能像处理数字一样轻易 的处理字符串。文件系统又是什么?也是一种伪装,假装硬盘并不是一堆不停 旋转,可以储存位的磁性盘片,而是一个有着层层目录的阶层式系统,可以存放 一个个由一或多个字节字符串构成的档案。 把话题拉回TCP。稍早为了让事情单纯一点,我撒了一个小谎,而且现在有些人 可能会因为这个谎气得头上冒烟。我说过TCP保证你的讯息会到达,其实并不会。 如果你养的蛇把连接计算机的网络线咬断了,就没存任何IP封包可以通过,这时 候TCP当然也不可能让你的讯息抵达。如果你惹毛了公司的系统管理员,他们为 了报复就把你接到已经超过负荷的集线器,因此只有部份的丨P封包能通过,这时 候TCP是会动,不过一切都会变得很慢。 这就是我称之为猶象树激存漏源的状况。TCP试图提供一个完整的抽象机制,想 隐藏底下不可靠的网络,不过有时候网络会渗漏越过抽象机制,这时就会觉得抽 象其实并不太能真的提供保护。这只是我所谓「抽象渗漏法则」的一个例子而已: 所有重大的抽象机制在某种程序上都是有漏洞的。 抽象会失效。有时候轻微有时候很严重,反正就是有漏洞。事情会因而出错,而 且当你有抽象机制时到处都可能会发生。下面有一些例子。 1. 像扫描一个大的二维数组这么简单的动作,是由水平方向或垂直方 向扫描都会严重影响效率,影响的大小依「木纹」(译注:二维数 组排列的方式)的方向而定,某个方向可能比另一个方向多产生许 多的分页失败,而分页失败是很慢的。虽然写汇编语言的程序员应 该可以假设自己拥有可连续寻址的内存空间,不过虚拟内存表示 这种假设只是种抽象机制而已。当出现分页失败时或是某些内存读 取时漏洞就会出现,处理时间会比其他内存慢几毫微秒。 2. SQL语言希望把数据库查询的程序抽象化,让你只要定义想要的东 西,查询动作的细节就交由数据库去处理。不过在某些状况下,有 些SQL查询比逻辑上相等的查询慢上几千倍。这有个很有名的例子, 在某个SQL服务器用”where a=b and b=c and a=c”来查询,会比用 “where a=b and b=c”快上许多,可是查询的结果其实是一样的。 照道理只要指定规格,并不需要在意程序。可是有时候抽象机制会 失效并导致很差的效率,于是你就得跳出来用查询规划分析器找 出问题,然后想办法加快查询。. 3. NFS或SMB之类的网络链接库,能让你「像」处理本机档案一样地处 理远程机器的档案。有时候连接速度会变得很慢或是断线,这时远 程档案就不再像是在本机上了,而身为程序员的你必须加程序代码 来处理这种状况。「远程档案和本地档案一样」的抽象机制出现漏 迥了。这里有个Unix系统管理员的具体例子。如果你把用户的home 目录放在用NFS挂入的磁盘上(一种抽象机制),而使用者建了一 个.forward档案把他们的电邮全部转寄到其他地方(另一种抽象 机制),如果新邮件进来时NFS服务器停掉了,由于找不到.forward 文件讯息并不会被转寄出去。这个抽象机制的漏洞就真的会把一 些讯息丢掉。 4. C++字符串类别应该能让你假装字符串是个第一级(first-class) 资料。它们尝试把「字符串很难处理」这个事实抽象掉,让它使用 上像整数一样容易。几乎所有C++字符串类别都会多载+运算符,才 能把字符串连接写成s + “bar”。不过你知道吗?不过怎么努力, 世上还是没有C++字符串类别能让你写成”foo” + “bar”,因为C++里的字符串常数一定是char*,绝对不会变成字符串。这个抽象机 制呈现一个程序语言本身不给补的漏洞。(有趣的是,C++随时间演 进的历史,可以描述成尝试用修补字符串抽象机制漏洞的过程。他 们为什么不直接在语言本身加个原生的字符串类别?这实在让我 搞不懂。) 5. 再来就是下雨天时开车没办法开得和平常一样快,虽然车上有挡风 玻璃雨刷有头灯有车顶还有暖气,这些装备应该是让你可以忽略下 雨这个事实(他们把天气抽象化了),不过看吧,你还是得担心天 雨路滑,有时候雨甚至会大到你看不远,所以在只好慢慢地开,因 为基于抽象渗漏法则,天气永远不能完全被抽象化。 抽象渗漏法则会造成问题的原因之一,是因为它说明了抽象机制并不真能照原构 想简化我们的生活。当我想训练某人成为C++程序员时,最好能完全不教char* 和指标运算,直接去学STL字符串。问题是总有一天他们会写出”foo” + “bar” 这样的程序然后看到怪事出现,于是我就得停下来教他们有关char*的事情。他 们也可能会试着呼叫某个需要OUT LPTSTR参数的Windows API函数,于是又得把 char*、指标、Unicode、wchar_t以及TCHAR含入档搞懂,才会知道如何呼叫。而 这些全都是漏洞。 在教COM程序设计时,最好只要教学生如何使用Visual Studio的精灵和各个程序 产生功能。不过万一出了任何问题,他们根本不会知道怎么回事,也不知道如何 除错或回复。我还是得教他们I Unknown和CLSI D还有ProgIDS以及。哦,饶了我吧! 在教ASP.NET程序设计时,最好只要教学生可以在组件上双击,然后就能撰写用 户点击该组件时在服务器执行的程序。不过处理超链接(`<a>`)点击事件的HTML 程序,和某个按钮被按时的处理程序是不一样的,而ASP.NET实际上是把这之间 的差异抽象化了。问题来了,ASP.NET的设计者必须把HTML无法由超链接传送表 格的事实隐藏起来。他们的做法是在超链接的onclick产理加上几行JavaScript 程序。不过这种抽象机制也有漏洞,如果用户关闭JavaScript功能,ASP.NET 的应用程序就不能正常的运作了,万一程序员又不了解ASP.NET抽象掉什么东西, 根本不可能知道出了什么问题。 抽象渗漏法则表示,当某人发明一套神奇的新程序产生工具,可以大幅提升效率 等等,就会听到很多人说:「应该先学会如何手动进行,然后才用这个神奇的 工具来节省时间。」程序产生工具假装抽象掉某些东西,和其他所有抽象机制 一样都有漏洞,而唯一能适当处理漏洞的方法,就是弄懂该抽象原理以及所隐藏 的东西。所以抽象机制虽然替我们节省了工作的时间,不过学习的时间是省不 掉的。 而这一切都似非而是地表示,即使我们拥有愈来愈高阶的程序设计工具,抽象化 也做得愈来愈好,要成为一个纯熟的程序员却是愈来愈难了。 我第一次去微软实习时,写了一个在麦金塔执行的字符串链接库。那是一个很典 型的任务:写一个自己的st「cat函数传回指向新字符串结尾的指针。只要写几行 C就够了。我做的每件事都写在K&R里面(一本讲C程序语言的薄书)。 今天为了要做CityDesk,我必须会Visual Basic、COM、ATL、C++、InnoSetup、Internet Explorer内部机制、正规表不式、DOM、HTML、CSS以及XML。一大堆比古老的K&R更高阶的工具,可是我还是得会K&R讲的东西,否则我就完了。 我们十年前可能想象过,现在会有某些全新的程序设计典范让程序设计更容易。 事实上这些年间所建立的抽象机制,游磁让我们能处理更高复杂度的软件开发 (如GUI程序设计和网络程序设计),这是十或十五年前无法处理的。这些伟大的 工具(比如00型式的程序语言)虽然能让我们用飞快的速度完成许多工作,不过 总会有一天我们得去追查因抽象渗漏而产生的问题,到时候就得查上两星期了。 另外虽然你得雇一个以写VB程序为主的程序员,不过单纯的VB程序员是不够的, 因为当VB的抽象机制渗漏时他们就完全卡住了。 抽象渗漏法则正在拖垮我们。