Archive for the '三句话不离本行' Category

8 年后重拾 DocBook

2011-4-19 22:01 | by 2ndboy

  最近 blog 更新实在是少,因为很多心得都直接进 wiki 了,其中一些可以在整理成熟后拿到 blog 里来做个总结,但是另外一些内容天生就更合适呆在 wiki 里。今天把最近研究 DocBook 的心得拿出来晒晒。

  记得是从 WorldHello 上知道 DocBook 这个好东西的,那时大概是 2003 年左右,当时貌似国内研究这个东西的人不多,而且一些相关的工具也不是太成熟,所以几经周折后也没有搭出来可以顺利工作的环境,后来也就没有继续研究下去了。去年把 wiki 翻出来用上以后,效果不错,所以前段时间又抽时间研究了一下 DocBook 这个同样在数年前曾经研究过一下的好东西。8 年间,光阴荏苒,DocBook 的主流已经不再是 SGML,而是变成了 DocBook 5.0 的 XML,相关的工具也更加成熟。下面就整理一下最近的学习成果,给出在 Windows 平台下搭建 DocBook 编译环境的方法,还有如何把 DocBook 源文件转换成包括 PDF 在内的各种常用格式的方法。

DocBook 介绍

  简单的讲,DocBook 是一种撰写文档(书籍、论文)的格式规范,它是 XML 的一种“方言”,只用来表达文档的结构性和内容,至于文档要被显示成什么样子,那是跟 DocBook 所配合的 XSL 和 processor 来决定的,有点像 HTML 跟 CSS 的关系。

  所以,用 DocBook 来写文档,你要按照 DocBook 的约定,用你喜欢的文本编辑器(当然也可以是有专门定制 GUI 的 tool)来编辑一个 XML 文件。当你想要把这个文档的内容放在网上供访客浏览的时候,就可以用 processor 把这个 DocBook 源文件转换成 HTML 格式;或者你想要分发或者打印这个文件的时候,你可以用 processor 把同一份 DocBook 源文件转换成 PDF 格式。

  这样做的好处在于:

  • 文本格式的文档方便进行 diff 和版本控制
  • 同一份源文档可以通过不同的处理方法得到各种格式的输出格式
  • 不会因为某种文件格式的过时而不能打开文档
  • 可以很方便的编辑,源文件内容可读
  • 方便进行本地化

  当然坏处也显而易见,DocBook 有一定的学习成本,不过比起它的好处来,这点成本可以忽略不计。目前有很多开源软件的文档和专业电子书都是用 DocBook 写成的,所以你会发现在看某些软件文档或者电子书的时候,同样的内容,经常会提供不同格式文件的下载,这一般都是通过 DocBook 来实现的。

环境需求

  要用 DocBook 来写文档,并且把它转换成你需要的格式,你需要这样一些东西:一个顺手的文本编辑器(比如 VimNotepad++ 或者 Emacs)、DocBook 的 XSL stylesheets 还有 XSLT processor 和 XSL-FO processor。

语法 & 编辑器

  文本编辑器就不用说了,操起你最习惯的就可以了。当然,你得先学习一下 DocBook 的语法,这里推荐一下权威的免费电子书《DocBook 5: The Definitive Guide》,你也可以在i18n-zh 的 Google Code 主页找到一个中译版本,不过已经许久不更新了。

DocBook XSL stylesheets

  接下来你可以到 DocBook Project 在 SourceForge 的页面下载最新版的 DocBook XSL stylesheets,我们把 DocBook 源文件转换到其它格式全靠它了!推荐下载 docbook-xsl-ns,它跟 docbook-xsl 的区别在于,docbook-xsl-ns 可以处理带有 namespace 的 DocBook 源文件(带有 namespace 的 DocBook 是 DocBook 5.0 的 new feature)。我写这篇文章的时候 docbook-xsl-ns 的最新版本是 1.76.1。假设你下载以后把它解压至 C:\DocBook\docbook-xsl-ns-1.76.1。

XSLT processor

  XSLT processor 用来根据 XSL 把 XML 格式的 DocBook 源文件转换成其它格式(比如 HTML 格式或者 .fo 格式(Format Objects,转换成 PDF 之前的中间格式))。在这里下载 xsltproc 的 for Windows 版,你一共需要下载 4 个包:iconv,libxml2,libxslt 和 zlib。假设你下载后把它们解压至 C:\DocBook\win32。

XSL-FO processor

  XSL-FO processor 用来把 XSL formatting objects 格式转换成 PDF 等格式。这里我们使用来自 Apache 软件基金会的 FOP。它目前的最新版本是 1.0。假设你下载以后把它解压至 C:\DocBook\FOP。

示例 DocBook 文件

  目前为止,我们的 DocBook 编译环境就已经搭好了,下面给出一个 DocBook 示例文件,把它保存成 C:\DocBook\2ndboy.xml,我们下面的内容都使用这个示例文件做例子:

  1. <?xml version='1.0' encoding="utf-8"?>
  2.  <article xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="zh-CN"
  3.      xmlns:xlink='http://www.w3.org/1999/xlink'>
  4.      <articleinfo>
  5.          <title>大标题</title>
  6.          <author>
  7.              <firstname></firstname>
  8.              <surname></surname>
  9.          </author>
  10.      </articleinfo> 
  11.      <section>
  12.          <title>标题</title>
  13.          <para>
  14.              这是我的第一篇Docbook 5.0文档,欢迎你来到<link xlink:href='http://blog.2ndboy.net/'>2ndboy 的 Blog</link>
  15.          </para>
  16.      </section>
  17.  </article>

DocBook to PDF

  如果是用来转换全英文的 DocBook 源文件,以上准备已经 OK 了,但是为了可以处理中文的 DocBook,还需要以下两个步骤:

让 FOP 自动识别并使用系统字体

  首先要让 FOP 可以找到并使用系统自带的字体(主要是中文字体)。把下面两行内容放到 C:\DocBook\FOP\conf\fop.xconf 里面的 <renderer mime=“application/pdf”><fonts> 节点里:

  1. <directory recursive="true">C:\Windows\Fonts</directory>
  2.  <auto-detect/>

第一句是指定字体文件的存放位置,并且递归搜索子目录。第二句是让 FOP 自动检测字体。

定制 PDF 的转换效果

为了控制转换后得到的 PDF 文件的最终效果(纸张类型,边距,字体等),我们需要定制一下用于转换过程中的 XSL stylesheet,只需要在之前下载的官方 XSL stylesheet 的基础上重载一些参数即可。把下面 XML 内容保存成 C:\DocBook\pdf.xsl:

  1. <?xml version='1.0'?>
  2.  <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  3.      xmlns:exsl="http://exslt.org/common"
  4.      xmlns:fo="http://www.w3.org/1999/XSL/Format"
  5.      xmlns:ng="http://docbook.org/docbook-ng"
  6.      xmlns:db="http://docbook.org/ns/docbook"
  7.      exclude-result-prefixes="db ng exsl"
  8.      version='1.0'>
  9.  
  10.      <xsl:import href="file:///C:/DocBook/docbook-xsl-ns-1.76.1/fo/docbook.xsl"/>
  11.  
  12.      <xsl:param name="body.font.family">SimSun</xsl:param>
  13.      <xsl:param name="body.font.size">12</xsl:param>
  14.      <xsl:param name="monospace.font.family">Consolas</xsl:param>
  15.      <xsl:param name="title.font.family">SimHei</xsl:param>
  16.      <xsl:param name="page.margin.inner">2cm</xsl:param>
  17.      <xsl:param name="page.margin.outer">2cm</xsl:param>
  18.      <xsl:param name="hyphenate">true</xsl:param>
  19.      <xsl:param name="paper.type" select="'A4'"/>
  20.      <xsl:param name="draft.mode" select="'no'"/>
  21.      <xsl:param name="fop1.extensions" select="1"/>
  22.  </xsl:stylesheet>

这里值得一提的是“fop1.extensions”这个参数,设成 1 以后可以生成有书签的 PDF 文件。

转换方法

把 DocBook 源文件转换成 PDF 格式需要分两步,首先把 XML 源文件转换成 .fo 格式:

cd C:\DocBook
win32\bin\xsltproc --encoding utf-8 -o 2ndboy.fo pdf.xsl 2ndboy.xml

然后再把 .fo 文件转换成 PDF 格式:

FOP\fop -c FOP\conf\fop.xconf -fo 2ndboy.fo -pdf 2ndboy.pdf

其实这两个步骤也可以直接通过 FOP 一步完成:

FOP\fop -c FOP\conf\fop.xconf -xsl pdf.xsl -xml 2ndboy.xml -pdf 2ndboy.pdf

只不过这样你就没办法调试 .fo 文件了。

DocBook to RTF

RTF(Rich Text Format)格式的文件可以用 Word 和写字板打开,也是一种常用的格式。把 DocBook 转成 RTF 格式同样需要两步:

win32\bin\xsltproc --encoding utf-8 -o 2ndboy.fo pdf.xsl 2ndboy.xml
FOP\fop -c FOP\conf\fop.xconf -fo 2ndboy.fo -rtf 2ndboy.rtf

为了方便我们这里直接使用了上面转换 PDF 时定制过的 XSL 文件。

DocBook to PNG

DocBook 文件还可以直接转成图片,比如常见的 PNG(Portable Network Graphics)格式,这里同样是两个步骤,为了偷懒同样直接使用我们用来转换 PDF 的那个 XSL 文件:

win32\bin\xsltproc --encoding utf-8 -o 2ndboy.fo pdf.xsl 2ndboy.xml
FOP\fop -c FOP\conf\fop.xconf -fo 2ndboy.fo -png 2ndboy.png

DocBook to HTML

用 DocBook 生成 HTML 输出有两种选择,一种是把所有内容放在一个单独的 HTML 文件里,另外一种是每一章节一个单独的 HTML 文件(就是所谓的 chunk HTML)。

生成单页 HTML

为了让生成的 HTML 文件不把汉字编码成 HTML 转义形式(比如 &#65288;),我们这里也需要重载一下用于 HTML 转换的 XSL 文件,下面内容另存为 C:\DocBook\html.xsl:

  1. <?xml version='1.0' encoding="utf-8"?>
  2.  <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  3.      xmlns:exsl="http://exslt.org/common"
  4.      xmlns:fo="http://www.w3.org/1999/XSL/Format"
  5.      xmlns:ng="http://docbook.org/docbook-ng"
  6.      xmlns:db="http://docbook.org/ns/docbook"
  7.      exclude-result-prefixes="db ng exsl"
  8.      version='1.0'>
  9.  
  10.      <xsl:import href="file:///C:/DocBook/docbook-xsl-ns-1.76.1/html/docbook.xsl"/>
  11.  
  12.      <xsl:output method="html" encoding="UTF-8" indent="no"/>
  13.  </xsl:stylesheet>

转换命令行如下:

win32\bin\xsltproc --encoding utf-8 --nonet -o 2ndboy.html html.xsl 2ndboy.xml

生成 chunk HTML

想要生成 chunk HTML 也需要重载 XSL,把下面内容另存为 C:\DocBook\chunkhtml.xsl:

  1. <?xml version='1.0' encoding="utf-8"?>
  2.  <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  3.      xmlns:exsl="http://exslt.org/common"
  4.      xmlns:fo="http://www.w3.org/1999/XSL/Format"
  5.      xmlns:ng="http://docbook.org/docbook-ng"
  6.      xmlns:db="http://docbook.org/ns/docbook"
  7.      exclude-result-prefixes="db ng exsl"
  8.      version='1.0'>
  9.  
  10.      <xsl:import href="file:///C:/DocBook/docbook-xsl-ns-1.76.1/html/chunk.xsl"/>
  11.  
  12.      <xsl:param name="chunker.output.encoding" select="'UTF-8'"/>
  13.  </xsl:stylesheet>

转换命令行如下:

win32\bin\xsltproc --encoding utf-8 --nonet -o 2ndboy.html chunkhtml.xsl 2ndboy.xml

生成的多个 HTML 文件会自动重命名。

DocBook to ePub

随着各种平板和电子阅读器的日渐流行,ePub 这种小屏幕友好的电子读物格式标准也越来越流行了,其实 DocBook 也可以被很方便的转换到 ePub 格式。我们重载一下 XSL,把下面内容另存为 C:\DocBook\epub.xsl:

  1. <?xml version='1.0'?>
  2.  <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  3.      xmlns:exsl="http://exslt.org/common"
  4.      xmlns:fo="http://www.w3.org/1999/XSL/Format"
  5.      xmlns:ng="http://docbook.org/docbook-ng"
  6.      xmlns:db="http://docbook.org/ns/docbook"
  7.      exclude-result-prefixes="db ng exsl"
  8.      version='1.0'>
  9.  
  10.      <xsl:import href="file:///C:/DocBook/docbook-xsl-ns-1.76.1/epub/docbook.xsl"/>
  11.  
  12.      <xsl:param name="body.font.family">SimSun</xsl:param>
  13.      <xsl:param name="monospace.font.family">Consolas</xsl:param>
  14.      <xsl:param name="title.font.family">SimHei</xsl:param>
  15.      <xsl:param name="hyphenate">true</xsl:param>
  16.      <xsl:param name="draft.mode" select="'no'"/>
  17.      <xsl:param name="fop1.extensions" select="1"/>
  18.  </xsl:stylesheet>

转换命令如下,为了方便,我把它写成了批处理。把下面内容另存为 C:\DocBook\epub.bat:


@echo off
win32\bin\xsltproc --encoding utf-8 epub.xsl %1.xml
echo.|set /P ="application/epub+zip" > mimetype
zip -0Xq %1.epub mimetype
zip -Xr9D %1.epub META-INF
zip -Xr9D %1.epub OEBPS
del /q/a/f/s META-INF\*.*
rmdir META-INF
del /q/a/f/s OEBPS\*.*
rmdir OEBPS
del mimetype

如果你想把 test.xml 转换成 test.epub,只需要在 C:\DocBook\ 下执行“epub test”即可。由于简单的讲,ePub 可以被看成是把一堆 XHTML 打包在一个 zip 压缩包里,所以这里用到了 zip,Windows 下的 zip 移植版可以在 gunwin32 下载。

有待解决的问题

目前还有些问题有待解决:

  1. 虽然 FOP 1.0 支持指定字体集,比如“<xsl:param name=”body.font.family”>Verdana,SimSun</xsl:param>”,但是这样写以后发现很多混杂在中文里的英文还是宋体,没法做到完美混合!
  2. 没法做到自动在中英文间插入一个空隙,中英文紧挨在一起,不好看,难道只能人肉插空格?

希望过路高手留言:)

[2011/7/29]:增加了 DocBook to ePub 的内容

DokuWiki – 知识管理(整理)工具有着落了

2010-9-27 21:14 | by 2ndboy

  最初接触 Wiki 是在 2004 年左右,那时候用的是 UseMod Wiki。当时就觉得 Wiki 是一个知识管理的好工具,而且非常方便多人协作来贡献信息(Google Docs 的多人实时协作编辑何尝不是一种对 Wiki 的增强呢?!),所以就想尝试用 Wiki 来管理和整理自己平时的一些积累。但是当时一来没有稳定可用的空间,二来我对当时接触到的一些 Wiki 系统不太透明的数据存放方式不太感冒,觉得编辑者无法完全掌控数据的存储层次关系,所以后来就没有了下文。

  最近工作上打算跟跨分公司的同事一起编辑一些文档,所以就又捡起 Wiki 来研究了一番,这一研究不要紧,让我这跨度达 6、7 年的寻找知识管理工具之旅有了个结果。

  一开始为了安装配置简单,所以就比较随便的找到了 PmWiki 来用,搜寻的主要方向当然是在我比较熟悉的 PHP 领域了,PmWiki 是用 PHP 实现的,而且不依赖于数据库,是把所有 page 保存在磁盘上的 TXT 文件中的。这一特性相当的吸引我!因为我在工作以后的一段时间里,逐渐形成了记工作日志的习惯,这样有些事情在时隔数月已经忘记的情况下仍然可以通过使用 Windows 自带的文件内容搜索回想起来。这些工作日志都是记在 TXT 文件里,按年份和月份分目录存放的。所以我现在仍然可以很轻松的查到自己在 2003 年的某一天里遇到和解决过什么棘手的问题:) 不得不表扬一下自己,这是个好习惯,虽然有时候你会觉得“浪费”了一点时间:)

  用了几天 PmWiki 之后偶然的看到了 Riku 的这篇“用 DokuWiki 打造个人知识管理系统”,于是很幸运的认识了 DokuWiki。(推荐另外一篇不错的比较 PmWiki 和 DokuWiki 的文章——“优秀的 Wiki 程序:PmWiki 和 DokuWiki”)

  试用之下惊喜异常:) 因为俺觉得,这下找到真正想要的东西了!跟 PmWiki 一样,DokuWiki 也是用文本文件来存储 Wiki pages 的,但是 DokuWiki 的 Wiki Page 文件是纯粹的 Wiki content,而 PmWiki 的 page 文件里同时混杂了 page 的修订历史记录,其实就凭这一条就能吸引住有数据洁癖的我:) DokuWiki 这种把 page 数据和 page 修订数据分开存放的方法其实相当科学,既方便了数据备份和迁移也方便了修订数据的清理(个人认为 Wiki 的这个类 VCS feature 在某些情况下很有用,但是在另外一些场合下是完全多余的)。另外 DokuWiki 对于中文的支持比 PmWiki 要好,虽然在 DokuWiki 里对中文 page name 或者 namespace 的支持也“不好”,但是个人认为这其实不算什么缺点:) DokuWiki 的 namespace 也深得我心,namespace 在 DokuWiki 里就是给 page 进行分类的,而在存储系统里它们二者就对应着文件夹和文件,这跟我记工作日志的习惯简直太一致了!大爱!

  其它优点还包括了数量众多的 plugin 支持,强大的 ACL 控制能力等等。说到 plugin,DokuWiki 里有很多 export to PDF plugin,这个特性也非常吸引我,想想你编辑和整理的众多信息可以很轻松的做成 PDF 文件保存和分发,而且所用的编辑语法又是简单的 Wiki 语法而非天书般的 DocBook,简直是太爽了!这方面,时间关系我只尝试了 dw2pdf,而且暂时没找到支持中文的方法,最终格式效果方面也还有瑕疵,就留待以后去研究了。

  我在空间里装上了 DokuWiki,地址是 http://wiki.2ndboy.net(也可以点 blog 顶上的链接进入),今后会陆续记录一些 blog 不方便和不适合记录的东西,欢迎有空坐坐:)

批处理的“函数调用”

2010-7-18 00:39 | by 2ndboy

  今天看 Visual Studio 2008 里环境设置批处理的时候学到一招,就是在批处理里面可以模拟出函数调用的效果,可以传参数也可以从“函数”里返回值,示例如下:

  1. @echo off
  2. call :foo 2ndboy
  3. call :foo World
  4.  
  5. goto :EOF
  6.  
  7. :foo
  8. echo Hello %1 !
  9. exit /b

说白了就是 call 现在直接支持调用本批处理里面的一个标签,而不是我们以前熟知的调用另外一个批处理。但是其实内部的原理还是把批处理自身当作另外一个批处理来调用(创建新的上下文),只不过是直接调用到了标签这个级别罢了。所以在被调用标签的最后的 exit /b 就相当于是函数调用里面的 return,跟 return 一样,exit 也可以把特定的值返回给调用方,比如用 exit /b 0 返回 0。这样一来调用方还可以根据返回值做不同的处理(if %ERRORLEVEL%==0 echo ok)。

exit /b 还可以用 goto :EOF 来代替,两者都可以从被调用标签内部返回到 call 命令的下一行继续执行,区别是 goto :EOF 不带返回值。

最近在断续实战 Hudson 的时候补了不少批处理命令的课,发现 Windows 命令行已经增强了很多,远非 DOS 时代我熟悉的那些有限功能。当然,离 *nix shell 或者 PowerShell 还是有差距的,不过它对环境的依赖也最少,有时候信手拈来用用还是不错滴。

又一个鄙视 IE 的理由

2010-4-8 20:35 | by 2ndboy

  以前曾经在 IE 6 上遇到过这种问题,用 Chrome 和 FireFox 都可以正常浏览的网站,有几幅图片在 IE 里始终显示叉叉。抓包分析后看上去 web server 那边一切正常,当时感觉这肯定是 IE 的毛病,不过没有深究。今天同事在 IE 8 上遇到了类似的问题,一起分析了下,结果又发现了一个鄙视 IE 的理由。

  我们的程序里内嵌了 IE 控件,但是如果频繁多次去访问 http://www.sina.com 的话,从第二次开始铁定是访问失败的。抓包后发现,凡是出现这种情况的时候,IE 发出去的 HTTP 请求报文都是不完整的,这个不完整不是说请求报文里少了什么字段,而是说假设把整个请求包报文看作一个 200 bytes 的 buffer 的话,IE 在一个单独的 TCP 包里只发出了比方说 100 bytes。其实这在协议上是允许的,遇到这种情况时,web server 要先把这部分请求报文存起来,等将来把整个请求报文收全了再做处理。

  估计是新浪为了防止 DoS 攻击吧,如果 client 发过去一个不完整的 request message,新浪 server 那边会马上回应一个 RST(Reset)过来,从而导致 HTTP 连接被重置,IE 不能显示网页。其实这也不能怪新浪,如果有人用程序在短时内恶意的发来海量不完整 HTTP request,server 又照单全收的缓存了这些半拉子请求的话,系统肯定会很快失去响应。看了一下,新浪用的 web server 是 nginx 0.8.35,果然。

  从抓包的结果看,Chrome 遇到这种连接被 server 端 reset 的情况时会重试 3 次,3 次都失败后才会放弃,所以网页/图片不能显示的情况就极少出现。从 Google 刚发布 Chrome 的时候我就开始从 Maxthon 转向 Chrome,现在除了如网银这类非用 IE 不可的情况外,我已经很少开 IE/Maxthon 了,这次的事情再次证明转向正确:)

–== 俺就一分割线 ==–

明天凌晨,Apple 就要发布 iPhone OS 4.0 了,希望盼望已久的多任务支持变成现实!明早起来再看新闻,希望不会失望:)

聊聊 API hooking

2009-12-14 22:30 | by 2ndboy

  今天接着说说 API hook。API hook 大致上有两种手段:修改 IAT 和修改 API 指令。那种用一个自己写的同名 DLL 来替换目标 DLL 的手段就不提啦,可操作性太差。今天聊一下直接修改 API 指令这种方法。

  直接修改 API 指令,简单的说就是在目标 API 代码的开始处修改一些指令,让执行绪跳转到自己的代码里面。这里需要面对的问题有 3 个:如何找到 API 的代码地址,如何修改代码段,如何构造执行绪转移指令。

  前两个问题比较简单,无非是相关 API 的调用,用 GetProcAddress() 可以取到目标 API 的代码所在地址;用 VirtualProtect() 可以修改代码所在地址的访问权限,有了写权限之后就可以对目标 API 的代码做手脚了。

  至于如何构造执行绪转移指令,其实说白了就是在目标 API 代码的开始处放置一条无条件跳转指令 jmp。对于 32bits 系统,jmp 的机器码是 0xE9(这里不讨论短跳转 0xEB 的原因是通常我们自己代码跟目标 API 代码之间的偏移都大于 128),加上 32bits 的相对地址,一条无条件跳转指令的长度为 5 个字节。也就是说我们要至少覆盖目标 API 开始处的 5 个字节,但是直接覆盖别人的代码是肯定会出问题地,所以我们在覆盖目标 API 开始处指令之前要把这些指令搬到其它地方去,后面还有用。

  对于 jmp 指令后面那个相对地址,不是简单的拿目标地址减去当前 jmp 指令的地址就行,由于 CPU 要执行过 jmp 指令,拿到跳转地址再进行跳转,所以计算相对地址的时候要用“目的跳转地址 – ( jmp 所在指令地址 + 5 )”。目前为止,把对目标 API 的调用劫持到我们自己的函数这个目的已经达到了。但是我们还是得在需要的时候调用原始 API 来完成 caller 想要完成的功能,不过别忘了原始 API 已经被我们破坏掉了,所以调用原始 API 还要靠我们之前保存的 API 代码起始处的那些指令。

  假设我们刚好从目标 API 处复制了 5 个字节的完整指令(注意一定要是完整指令)放在内存 A 处,那么在 A + 5 处再构造一条跳转指令跳回到 API + 5 处就等于是把断成两截的 API 代码又连接了起来。这样一来,在需要调用原本的 API 功能时我们要调用的其实是 A。

以下是一些示例代码:

  1. typedef HRESULT (WINAPI *_DirectDrawCreate)( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter );
  2.  _DirectDrawCreate pOldDirectDrawCreate = NULL;
  3.  
  4.  HRESULT WINAPI
  5.  MyDirectDrawCreate( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter )
  6.  {
  7.      MessageBox( NULL, "MyDirectDrawCreate", "MyDirectDrawCreate", 0 );
  8.      return( pOldDirectDrawCreate( lpGUID, lplpDD, pUnkOuter) );
  9.  }
  10.  
  11.  int main( int argc, char *argv[] )
  12.  {
  13.      HMODULE hDll = LoadLibrary( "DDraw.dll" );
  14.      if( hDll )
  15.      {
  16.          pOldDirectDrawCreate = (_DirectDrawCreate)GetProcAddress( hDll, "DirectDrawCreate" );
  17.          HookAPI( (PVOID *)(&pOldDirectDrawCreate), MyDirectDrawCreate );
  18.      }
  19.  
  20.      LPDIRECTDRAW pDDraw = NULL;
  21.      HRESULT hr = DirectDrawCreate( NULL, &pDDraw, NULL );
  22.      if( DD_OK == hr )
  23.          hr = pDDraw->SetCooperativeLevel( NULL, DDSCL_NORMAL );
  24.  
  25.      return( 0 );
  26.  }

以上代码成功执行后,当我们调用 DirectDrawCreate() 的时候会被之前设置的 hook 函数截获,显示一个对话框。注意 HookAPI() 的第一个参数是指向指针的指针,所以我们开始时拿到的“原始” API 地址其实被换掉了(见上面描述,原始 API 代码已经被我们断成两截啦)。

下面是代码里用到的两个关键数据结构:

  1. #pragma pack (1)
  2.  struct HookTarget
  3.  {
  4.      char jmp;
  5.      int  address;
  6.  
  7.      HookTarget()
  8.      {
  9.          jmp = char( 0xE9 );
  10.      }
  11.  };
  12.  
  13.  struct HookThunk
  14.  {
  15.      char instructions[16];
  16.      HookTarget HookProc;
  17.  
  18.      HookThunk()
  19.      {
  20.          memset( instructions, 0x90, sizeof( instructions ) );
  21.      }
  22.  };
  23.  #pragma pack ()

HookTarget 是用来写覆盖在目标 API 起始处的无条件跳转指令的,HookThunk 是用来把断成两截的 API 代码连起来的。0x90 是 nop 的机器码,就算我们只从目标 API 那里复制了 5 个字节,后面的 nop 也会安全的让执行绪运行到 jmp 指令处。

下面是 HookAPI() 的代码:

  1. bool
  2.  HookAPI( PVOID *ppProc, LPCVOID pMyProc )
  3.  {
  4.      do
  5.      {
  6.          DWORD dwOldProtect = 0;
  7.          if( !RemoveReadOnly( *ppProc, 16, &dwOldProtect ) )
  8.              break;
  9.  
  10.          HookThunk *pThunk = new HookThunk;
  11.          int cbInstructions = CheckInstructions( (LPBYTE)*ppProc );
  12.          memcpy( pThunk, *ppProc, cbInstructions );
  13.          pThunk->HookProc.address = ( (int)*ppProc + cbInstructions )
  14.              - ( (int)pThunk + sizeof( HookThunk ) );
  15.  
  16.          DWORD dwTemp = 0;
  17.          if( !VirtualProtect( pThunk, sizeof( HookThunk ), dwOldProtect, &dwTemp ) )
  18.              break;
  19.  
  20.          HookTarget *pOriginalProc = (HookTarget *)*ppProc;
  21.          pOriginalProc->jmp     = char( 0xE9 );
  22.          pOriginalProc->address = (int)MyDirectDrawCreate - ( (int)pOriginalProc + 5 );
  23.  
  24.          if( !VirtualProtect( *ppProc, 16, dwOldProtect, &dwTemp ) )
  25.              break;
  26.  
  27.          *ppProc = pThunk;
  28.          return( true );
  29.      }
  30.      while( false );
  31.  
  32.      return( false );
  33.  }

就不详细解释了,看完了上面那一大坨原理分析,HookAPI() 这个函数很容易看懂。上面代码里面的 RemoveReadOnly() 就是对 VirtualProtect() 调用的包装而已,对于我们这个示例的目标 API——DirectDrawCreate(),CheckInstructions() 简单的返回 5 就可以了(如果程序执行异常,最好自己用调试器看一下指令边界在哪里)。

  注意以上的示例代码只是用来做测试之用,没有做完备的错误处理。而且在实际环境中目标 API 代码的起始指令边界不一定正好是 5 个字节,如果边界是 4 个字节和 6 个字节处,由于我们至少要覆盖 5 个字节,所以要多复制一些(复制 6 个字节)。另外代码中使用的指令集和地址都只适用于 32bits x86 系统,在其它 CPU 和 OS 上要做相应的修改才能正常运行。最后,在实际的环境中,还要写卸载 hook 的代码;)