青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

loop_in_codes

低調(diào)做技術(shù)__歡迎移步我的獨(dú)立博客 codemaro.com 微博 kevinlynx

#

博客遷移

博客遷移

3年前我從CSDN博客遷移到CPPBLOG寫(xiě)博客。然后我亂七八糟地寫(xiě)一篇博客聲明。那個(gè)時(shí)候還 沒(méi)把寫(xiě)博客當(dāng)一回事,純碎是出于為自己做記錄的目的。如今寫(xiě)博客一半還是出于記錄,另 一半則是出于在外面自我表現(xiàn),還有那么一點(diǎn)點(diǎn)說(shuō)出來(lái)覺(jué)得崇高的理由就是分享自己的知識(shí) 。

這回我倒不是遷移出CPPBLOG。CPPBLOG整體上是個(gè)更低調(diào)做技術(shù)的圈子,一如我博客的副標(biāo) 題。自己建了個(gè)獨(dú)立博客,主要還是因?yàn)閷W(xué)習(xí)Lisp的原因。

在過(guò)去的若干年中,有各種各樣的技術(shù)點(diǎn)因?yàn)殚L(zhǎng)久的疏遠(yuǎn),一個(gè)一個(gè)淡出于大腦。雖說(shuō)技術(shù) 點(diǎn)不大重要,學(xué)習(xí)能力、方法才是終生受用的東西。但完全忘記,也算是一大遺憾。Lisp不 應(yīng)該被我忘記,一個(gè)程序員至少應(yīng)該在他常用的語(yǔ)言之外,熟悉另一門(mén)語(yǔ)言。而這另一門(mén)語(yǔ) 言應(yīng)該是更“快速的“、更“方便的“,它能在你需要的時(shí)候更快速地構(gòu)建你的想法。雖然出于 工作原因,我得時(shí)不時(shí)地?fù)v鼓下Lua。

Anyway,這個(gè)獨(dú)立博客就是我用Lisp搭建的。從Web server到博客系統(tǒng)。其中博客系統(tǒng)也有 我很大一部分代碼在其中,在可預(yù)見(jiàn)的未來(lái),在Lisp應(yīng)用方面我將主要用于構(gòu)建這個(gè)博客系 統(tǒng)。靠自己寫(xiě)的代碼(雖然只有一部分)搭建出來(lái)的博客就是爽啊。如果要我用個(gè)現(xiàn)成的, 我還真懶得去獨(dú)立門(mén)戶。

程序員嘛,只有在不斷地自我折騰(簡(jiǎn)寫(xiě)自虐)中,才能成長(zhǎng)。最后,請(qǐng)求大家(+強(qiáng)迫) 移步我的新博客 codemacro.com 。要參考技術(shù)信息的,可以 參考這里 ,要看看我使用 Lisp是如何搗鼓出這個(gè)博客系統(tǒng)的,可以 點(diǎn)這里 。當(dāng)然,在可預(yù)見(jiàn)的未來(lái),我還是會(huì)在 CPPBLOG同步更新的。 。

posted @ 2011-04-24 17:30 Kevin Lynx 閱讀(4419) | 評(píng)論 (7)編輯 收藏

傳遞Lua函數(shù)到C/C++中

传递Lua函数到C/C++中

问题

在Lua中,因为函数也是第一类值,所以会出现将函数作为另一个函数的参数,或者函数作 为函数的返回值。这种机制在很多地方都能代码更灵活更简洁,例如:

table.sort(table [,comp])

这里的comp就要求传入一个函数,我们在调用时,大概会有如下形式:

table.sort(t, comp) -- 直接写函数名
table.sort(t, local_comp) -- 某个局部函数
table.sort(t, function (a, b) xxx end ) -- 临时构造一个匿名函数

其中最后一种方式最为灵活,任意时候在需要的时候构造一个匿名函数。这种在Lua自身的 环境中使用,自然没有问题。但是,当我们在C/C++中注册一些函数到Lua环境中,而这些 函数也需要使用函数参数的时候,问题就出来了。

Lua本身是不支持将Lua函数作为函数参数传入C/C++的,不管这个想要传入的函数是全局的 、局部的、或者匿名的(匿名的本质上也算局部的)。一般情况下,我们唯一的交互方式, 不是传入一个函数,而是一个全局函数名。C/C++保存这个函数名,在需要回调Lua的时候, 就在Lua全局表中找到这个函数(根据函数名),然后再调用之。情况大致如下:

function lua_func () xxx end
cfunc(lua_func) -- wrong
cfunc("lua_func") -- right

我们这回的脚本模块,策划会大量使用需要回调函数的C/C++函数。显然,创建大量的全局 函数,先是从写代码的角度看,就是很伤神的。

解决

我们最终需要的方式,大概如下:

cfunc(lua_func) -- ok
cfunc(function () xxx end) -- ok
local xxx = function () xxx end
cfunc(xxx) -- ok

要解决这个问题,我的思路是直接在Lua层做一些包装。因为C/C++那边仅支持传入一个全局 函数名(当然不一定得全局的,根据实际情况,可能在其他自己构造的表里也行),也就是 一个字符串,所以我的思路就是将Lua函数和一个唯一的字符串做映射。:

function wrap (fn)
    local id = generate_id()
    local fn_s = "__callback_fn"..id
    _G[fn_s] = fn
    return fn_s
end

这个wrap函数,就是将一个函数在全局表里映射到一个字符串上,那么在使用时:

cfunc(wrap(function () xxx end))
cfunc(const char *fn_name, xxx); -- cfunc的原型

cfunc是C/C++方注册进Lua的函数,它的原型很中规中矩,即:只接收一个函数名,一个字 符串,如之前所说,C/C++要调用这个回调函数时,就根据这个字符串去查找对应的函数。 脚本方在调用时,如果想传入一个匿名函数了,就调用wrap函数包装一下即可。

一个改进

上面的方法有个很严重的问题,在多次调用wrap函数后,将导致全局表也随之膨胀。我们需 要想办法在C/C++完成回调后,来清除wrap建立的数据。这个工作当然可以放到C/C++来进行 ,例如每次发生回调后,就设置下全局表。但这明显是不对的,因为违背了接口的设计原则 ,这个额外的机制是在Lua里添加的,那么责任也最好由Lua来负。要解决这个问题,就可以 使用Lua的metamethods机制。这个机制可以在Lua内部发生特定事件时,让应用层得到通知。 这里,我们需要关注__call事件。

Lua中只要有__call metamethod的值,均可被当作函数调用。例如:

ab(1, 2)

这里这个函数调用形式,Lua就会去找ab是否有__call metamethod,如果有,则调用它。这 个事实暗示我们,一个table也可以被调用。一个改进的wrap函数如下:

local function create_callback_table (fn, name)
    local t = {}
    t.callback = fn
    setmetatable (t, {__call =  -- 关注__call
        function (func, ...) -- 在t(xx)时,将调用到这个函数
            func.callback (...) -- 真正的回调
            del_callback (name) -- 回调完毕,清除wrap建立的数据
        end })
    return t
end

function wrap (fn)
    local id = generate_func_id() -- 产生唯一的id
    local fn_s = "_callback_fn"..id
    _G[fn_s] = create_callback_table(fn, fn_s) -- _G[fn_s]对应的是一个表
    return fn_s
end

在我们的C/C++程序中,依然如往常一样,先是从_G里取出函数名对应的对象。虽然这个对 象现在已经是一个table。然后lua_call。

上面的代码是否会在原有基础上增加不可接受的性能代价?虽然我没有做过实际测试,但是 从表明看来,排除meta table在Lua里的代价,也就多了几次Lua函数调用。

最后,感叹一下,Lua里的table及metatable机制,实在非常强大。这种强大不是功能堆砌 出来的强大,而是简单东西组合出来的强大。其背后的设计思想,着实让人佩服。

4.26.2011 Update

之前的文中说“Lua本身是不支持将Lua函数作为函数参数传入C/C++的“,这句话严格来说不 正确(由某网友评论)。假设函数cfun由c/c++注册,我们是可以编写如下代码的:

cfunc(print) -- 传入Lua函数

但是问题在于,我们无法取出这个函数并保存在c/c++方。Lua提供了一些接口用于取cfunc 的参数,例如luaL_checknumber(封装lua_tonumber)。但没有类似luaL_checkfunction的 接口。Lua中的table有同样的问题。究其原因,主要是Lua中的函数没有直接的c/c++数据结 构对应。

;; END

posted @ 2011-04-24 17:28 Kevin Lynx 閱讀(11591) | 評(píng)論 (9)編輯 收藏

淺談代碼分層:構(gòu)建模塊化程序

浅谈代码分层:构建模块化程序

Author:Kevin Lynx
Date:4.4.2011
Contact:kevinlynx at gmail dot com

模块化的程序是怎样的程序?我们可以说一个具有明显物理结构的软件是模块化的,例如带 插件的软件,一个完整的软件由若干运行时库共同构建;也可以说一个高度面向对象的库是 模块化的,例如图形引擎OGRE;也可以说一些具有明显层次结构的代码是模块化的。

模块化的软件具有很多显而易见的好处。在开发期,一个模块化的设计有利于程序员实现, 使其在实现过程中一直保持清晰的思路,减少潜伏的BUG;而在维护期,则有利于其他程序 员的理解。

在我看来,具有良好模块设计的代码,至少分为两种形式:

  • 整体设计没有层次之分,但也有独立的子模块,子模块彼此之间耦合甚少,这些子模块 构成了一个软件层,共同为上层应用提供服务;
  • 整个库/软件拥有明显的层次之分,从最底层,与应用业务毫无相关的一层,到最顶层, 完全对应用进行直接实现的那一层,每一个相对高层的软件层依赖于更底层的软件层, 逐层构建。

上述两种形式并非完全分离,在分层设计中,某一层软件层也可能由若干个独立的模块构成 。另一方面,这里也不会绝对说低层模块就完全不依赖于高层模块。这种双向依赖绝对不是 好的设计,但事实上我们本来就无法做出完美的设计。

本文将代码分层分为两大类:一是狭义上的分层,这种分层一般伴有文件形式上的表现;一 是广义上的分层,完全着眼于我们平时写的代码。

软件分层

软件分层一般我们可以在很多大型软件/库的结构图中看到。这些分层每一层本身就包含大 量代码。每个模块,每一个软件层都可能被实现为一个运行时库,或者其他以文件形式为 表现的东西。

Example Android

Android是Google推出的智能手机操作系统,在其官方文档中有Android的系统架构图:

imgs/android-architecture.jpg

这幅图中很好地反映了上文中提到的软件层次。整个系统从底层到高层分为Linux kernel, Libraries/Runtime,Application Framework,Applications。最底层的Kernel可以说与应 用完全不相关,直到最上层的Applications,才提供手机诸如联系人、打电话等应用功能。

每一层中,又可能分为若干相互独立(Again,没有绝对)的模块,例如Libraries那一层 中,就包含诸如Surface manager/SGL等模块。它们可能都依赖于Kernel,并且提供接口给 上层,但彼此独立。

Example Compiler

在编译器实现中,也有非常明显的层次之分。这些层次可以完全按照编译原理理论来划分。 包括:

  • 词法分析:将文本代码拆分为一个一个合法的单词
  • 语法分析:基于 词法分析 得到的单词流构建语法树
  • 语义分析:基于 语法分析 得到的语法树进行语义上的检查等
  • 生成器:基于 语义分析 结果(可能依然是语法树)生成中间代码
  • 编译器:基于 生成器 得到的中间代码生成目标机器上的机器代码
  • 链接器:基于 编译器 生成的目标代码链接成最终可执行程序

软件分层的好处之一就是对任务(task)的抽象,封装某个任务的实现细节,提供给其他 依赖模块更友好的使用接口。隔离带来的好处之一就是可轻易替换某个实现。 例如很 多UI库隔离了渲染器的实现,在实际使用过程中,既可以使用Direct X的渲染方式,也可 以使用OpenGL的实现方式。

但正如之前所强调,凡事没有绝对,凡事也不可过度。很多时候无法保证软件层之间就是单 向依赖。而另一些时候过度的分层也导致我们的程序过于松散,效率在粘合层之间绕来绕去 而消失殆尽。

代码分层

如果说软件分层是从大的方面讨论,那么本节说的代码分层,则是从小处入手。而这也更是 贴近我们日常工作的地方。本节讨论的代码分层,不像软件分层那样大。每一层可能就是 百来行代码,几个接口。

Example C中的模块组织

很多C代码写得少的C++程序员甚至对一个大型C程序中的模块组织毫无概念。这是对其他技 术接触少带来的视野狭窄的可怕结果。

在C语言的世界里,并不像某些C++教材中指出的那样,布满全局变量。当然全局变量的使 用也并不是糟糕设计的标志(goto不是魔鬼)。一个良好设计的C语言程序懂得如何去抽象、 封装模块/软件层。我们以Lua的源代码为例。

lua.h文件是暴露给Lua应用(Lua使用者)的直接信息源。接触过Lua的人都知道有个结构体 叫lua_State。但是lua.h中并没有暴露这个结构体的实现。因为一旦暴露了实现,使用者就 可能会随意使用其结构体成员,而这并不是库设计者所希望的。 封装数据的实现,也算 是构建模块化程序的一种方法。

大家都知道暴露在头文件中的信息,则可能被当作该头文件所描述模块的接口描述。所以, 在C语言中任何置于头文件中的信息都需要慎重考虑。

相对的,我们可以在很多.c文件中看到很多static函数。例如lstate.c中的stack_init。 static用于限定其修饰对象的作用域,用它去修饰某个函数,旨在告诉:这个函数仅被当前文件( 模块)使用,它仅用于本模块实现所依赖,它不是提供给模块外的接口! 封装内部实现 ,暴露够用的接口,也是保持模块清晰的方式之一。

良好的语言更懂得对程序员做一种良好设计的导向。但相对而言,C语言较缺乏这方面的语 言机制。在C语言中,良好的设计更依赖于程序员自己的功底。

Example Java中的模块组织

相较而言,Java语言则提供了模块化设计的语法机制。在Java中,如同大部分语言一样,一 般一个代码文件对应于一个代码模块。而在Java中,每个文件内只能有一个public class。 public class作为该模块的对外接口。而在模块内部,则可能有很多其他辅助实现的class ,但它们无法被外部模块访问。这是语言提供的封装机制,一种对程序员的导向。

Example OO语言中类接口设计

无论在C++中,还是在Java中,一个类中的接口,都大致有各种访问权限。例如public、 private、protected。访问权限的加入旨在更精确地暴露模块接口,隐藏细节。

在C中较为缺乏类似的机制,但依然可以这样做。例如将结构体定义于.c文件中,将非 接口函数以static的方式实现于.c文件中。

OO语言中的这些访问权限关键字的应用尤为重要。C++新手们往往不知道哪些成员该public ,哪些该private。C++熟手们在不刨根挖底的情况下,甚至会对每个数据成员写出get/set 接口(那还不如直接public)。在public/private之间,我们需要做的唯一决策就是,哪些 数据/操作并非外部模块所需。如果外部模块不需要,甚至目前不需要,那么此刻,都不要 将其public。一个public信息少的class,往往是一个被使用者更喜欢的class。

(至于protected,则是用于继承体系之间,类之间的信息隐藏。)

Example Lisp中的模块设计

又得提提Lisp。

基于上文,我们发现了各种划分模块、划分代码层的方式,无论是语言提供,还是程序员自 己的应用。但是如何逐个地构建这些层次呢?

Lisp中倡导了一种更能体现这种将代码分层的方式:自底而上地构建代码。这个自底而上, 自然是按照软件层的高低之分而言。这个过程就像上文举的编译原理例子一样。我们先编写 词法分析模块,该模块可能仅暴露一个接口:get-token。然后可以立马对该模块进行功能 测试。然后再编写语法分析模块,该模块也可能只暴露一个接口:parse。语法分析模块建 立于词法分析模块之上。因为我们之前已经对词法分析模块进行过测试,所以对语法分析的 测试也可以立即进行。如此下去,直至构建出整个程序。

每一个代码层都会提供若干接口给上层模块。越上层的模块中,就更贴近于最终目标。每一 层都感觉是建立在新的“语言“之上。按照这种思想,最终我们就可以构建出DSL,即Domain Specific Language。

分层的好处

基于以上,我们可以总结很多代码分层的好处,它们包括(但不限于):

  • 隐藏细节,提供抽象,隐藏的细节包括数据的表示(如lua_State)、功能的实现
  • 在新的一层建立更高层的“语言”
  • 接口清晰,修改维护方便
  • 方便开发,将软件分为若干层次,逐层实现

一个问题的解决

有时候,我们的软件层很难做到单向依赖。这可能是由于前期设计的失误导致,也可能确实 是情况所迫。在很多库代码中,也有现成的例子。一种解决方法就是通过回调。回调的实现 方式可以是回调函数、多态。多态的表现又可能是Listener等模式。

所有这些,主要是让底层模块不用知道高层模块。在代码层次上,它仅仅保存的是一个回调 信息,而这个信息具体是什么,则发生在运行期(话说以前给同事讲过这个)。这样就简单 避免了底层模块依赖高层模块的问题。

END

精确地定义一个软件中有哪些模块,哪些软件层。然后再精确地定义每个模块,每个头文件 ,每个类中哪些信息是提供给外部模块的,哪些信息是私有的。这些过程是设计模块化程 序的重要方式。

但需要重新强调的是,过了某个度,那又是另一种形式的糟糕设计。但其中拿捏技巧,则只 能靠实践获取。

posted @ 2011-04-05 10:12 Kevin Lynx 閱讀(19046) | 評(píng)論 (5)編輯 收藏

Lisp實(shí)踐:開(kāi)發(fā)RSS閱讀器

Lisp实践:开发RSS阅读器

Author:Kevin Lynx
Date:3.30.2011
Contact:kevinlynx at gmail dot com

Tip

本文简要介绍了如何使用Lisp实现一个简单的RSS阅读器,对Lisp无兴趣的TX可以 只对RSS阅读器的实现思路稍作了解即可。

一、RSS阅读器的实现

RSS Reader的实现并不像它看上去那么复杂。当初在决定写这个作为Lisp练习时,甚至觉得 没有多少内容可做。其简单程度甚至用不了你启动一个慢速IDE的时间:D。对Lisp无兴趣的 TX只需要读完这一节即可,

什么是RSS阅读器?

RSS在实现上,可以说是XML的又一次扩张式的应用。因为RSS最重要的东西就是一个XML文件 。RSS主要用于Web中的内容同步。例如我们写的博客,门户网站的新闻,都是内容。Web服 务器将这些内容组织成XML,然后我们通过一个客户端来解析这些XML,就可以在不用直接访 问网站的情况下获取信息:

imgs/rss-overview.png

RSS阅读器就是这样一个从Web服务器通过RSS(表现形式为XML)来获取信息内容的工具。它 可以被实现为一个独立的客户端程序,也可以实现为像Google Reader这种网页形式。后者 其核心功能其实是Google服务器在做,取得信息后再发给用户。

RSS文件

上已提及,RSS的实现其实就是个XML文件。这个XML文件格式非常简单,例如:

<?xml version="1.0"?>
<rss version="2.0">
   <channel>
      <title>Liftoff News</title>
      <link>http://liftoff.msfc.nasa.gov/</link>
      <description>Liftoff to Space Exploration.</description>
      <item>
         <title>Star City</title>
         <link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
         <description>Oh no, you wrote another blog!</description>
      </item>
    </channel>
</rss>

我们身边到处都是RSS文件,例如 http://m.shnenglu.com/rss.aspx 。RSS文件的框架大 致为:

<rss>
    <channel>
        <item>
        </item>
        <item>
        </item>
        ...
    </channel>
</rss>

对,其框架就是这样,一个channel节点,其下若干个item节点。举例来说, CPPBLOG首页就 是一个channel,该channel下有若干原创文章,每篇文章就是一个item。 无论是channel ,还是item,都会有很多属性,例如title/description/link,有些属性是RSS规范里要求 必须有的,有的是可选的。

交互过程

那么,服务器和客户端是如何交互的呢?首先,服务器上的程序针对其某个页面,生成对应 的RSS文件。这个RSS文件基本上是有固定的URL的。客户端每次获取内容时,就从这个固定 的URL获取这个RSS文件。客户端获取到这个RSS文件后,解析之,再呈现给用户。这就是整 个过程。这个过程中客户端与服务器的交互,全部是正常的HTTP请求。

而RSS阅读器,如果做得足够简单,则只需要从指定的地方获取到RSS文件,然后解析这个 XML文件,然后以相对友好的形式显示即可。

扩展

虽然RSS阅读器在核心功能上很简单,但是作为一个可以使用的工具,依然有很多功能点需 要实现。基本功能包括:

  • 记录用户关注的RSS
  • 缓存某个RSS过去一段时间更新的内容
  • 对HTTP回应的处理,最重要的就是重定向处理

我们还可以做很多扩展,例如Google Reader之类的在线RSS阅读器。这些阅读器的RSS抓取 功能做在服务器端,它们除了上面提到的基础功能外,还会包含内容分类,给内容打一些 标签,分析用户的订阅习惯然后推荐类似的内容等等。

二、Lisp实现

本节描述在Lisp中实现上文的内容。主要包括: 通过HTTP请求获取到RSS文件、解析RSS文件 。

获取RSS文件

Lisp虽然历史悠久,但其扩展库标准却做得很拙劣。偏应用级的扩展库要么由编译器实现提 供,要么就得自己在网上找。一方面使用者希望库使用起来方便,另一方面库开发者在跨编 译器实现方面也头疼不已。所幸现在有了quick lisp,安装第三方库就像Ubuntu里安装软件 一样简单(大部分)。

socket支持就是面临的第一个问题。不过我这里并不考虑跨编译器实现的问题,直接使用 SBCL里的socket接口。

要获取RSS文件,只需要连接Web服务器,发起HTTP的GET请求即可。当然,建立TCP连接,组 建HTTP请求包,就不是这里的讨论了。我们还是拿CPPBLOG首页的RSS为例,该RSS的URL为:

http://m.shnenglu.com/rss.aspx

拆分一下,得到host为m.shnenglu.com(即我们要connect的地址),rss的uri为 /rss.aspx(即HTTP请求里的文件URI),于是建立HTTP请求包:

GET /rss.aspx HTTP/1.0
Host: m.shnenglu.com

关于HTTP请求的一些基础知识,可以参考我很早前写的一篇博客:<实现自己的http服务器>。 正常情况下,Web服务器就会返回RSS的文件内容。然后我们就可以继续解析。

解析RSS

RSS本身是一个XML格式的文件。之前连接Web服务器发起HTTP请求没有用到第三方库,但是 解析XML文件不是几十来行代码能搞定的事情,所以这里需要选用一个第三方库。

我用的是s-xml,这个库在我之前的 关于Lisp的文章 中提到过。s-xml与我之前在C++ 领域见到的XML解析库最大的不同点在于,它提供的API是基于事件模式的。意思是说,你不 要去查询某个element的值是多少,当我解析到的时候会告诉你。事件模式的编程方式自然 离不开回调函数:

(s-xml:start-parse-xml
  stream
  (make-instance 's-xml:xml-parser-state
                 :new-element-hook #'decode-rss-new-element
                 :finish-element-hook #'decode-rss-finish-element
                 :text-hook #'decode-rss-text)))

与s-xml交互的也就是上面代码里提到的三个函数:new-element-hook, finish-element-hook , text-hook。这种类型的接口导致解析代码大量减少,但不利于理解。我们要在整个解析 过程中传递数据,需要通过与s-xml交互的函数参数(当然不会蠢到去用全局变量)。

解析过程中通过往函数参数指定的对象身上塞数据完成,整个解析实现也就几十行代码。 文章尾可下载代码看看。

显示出来

通过上面两步,我们得到了RSS文件、解析出了具体内容,最后一步就是呈现出来看看。RSS 文件里每个Item都是一篇文章(新闻之类),这个文章内容可直接包含HTML标记,说白了, 这些内容就是直接的HTML内容。要显示这些内容,最简单的方法就是把一个RSS转换成一种 简单的HTML文件,以供阅读。

这里就涉及到HTML generator,几乎所有的Lisper都会写一个HTML产生器(库)(虽然目前 我还没写)。这种库的作用就是方便地输出HTML文件。

Lisp相对于其他语言很大的一个特点,或者说是优点,就是其语言本身的扩展能力。这种扩 展不是简单的添加几个函数,也不是类,而是提供一些就像语言本身提供的特殊操作符一样 的东西。而HTML generator正是这种东西大放异彩的地方。这种感觉有点像在C++中通过模 板造出各种增强语言特性的东西一样(例如boost/loki)。

因为我这里只是输出简单的HTML文件,何况我对HTML的标记了解的也不多,也懒得再花经历 。所以我暂时也就将就了些土方法:

(with-output-to-string (stream)
  (let ((channel (rss-channel rss))) ;取出channel对象
   (format stream "<html><head><title>~a</title></head>"
           (get-property channel :|title|)) ;取出channel的title

最后组合一些接口,即可将整个过程联系起来,导出html文件:

(cl-rss-test:test-rss-http :uri "/news/newshot/hotnewsrss.xml"
                           :host "cd.qq.com")

然后在浏览器里查看,如图:

imgs/screenshot.png

其他

当一些代码可以工作起来的时候,就可以着手测试这批代码。然后我就用这个工具测试我 Google Reader里订阅的一些RSS。最大的问题,就是关于HTTP重定向的问题。

当服务器返回301或者302的错误信息时(HTTP回应),就标示请求的URI被移动到了其他地 方,客户端需要访问新的地址。这个其实查查 HTTP的规范 就可以轻易解决。重定向时, 新的URI被指定在Response Header里的Location域,取出来发起第二次请求即可。

下载代码

posted @ 2011-03-30 09:32 Kevin Lynx 閱讀(4101) | 評(píng)論 (0)編輯 收藏

Lisp一瞥:增強(qiáng)型變量Symbol

Lisp一瞥:增強(qiáng)型變量Symbol

Author: Kevin Lynx
Date: 3.21.2011
Contact: kevinlynx at gmail dot com

Note

本文描述的Lisp主要指Lisp的方言Common Lisp。

變量,是所有編程語(yǔ)言里都有的語(yǔ)法概念。在C/C++中,變量用于標(biāo)示一個(gè)內(nèi)存地址,而變 量名則在語(yǔ)法層面上代表這個(gè)地址。當(dāng)鏈接器最終鏈接我們的程序時(shí),就將這些名字替換 為實(shí)際的地址。在其他語(yǔ)言中,變量雖然或多或少有其他不同的含義,但也大致如此。

Lisp中的變量也差不多這樣,但若將variable和Lisp中的 symbol 放在一起,則多少會(huì) 帶來(lái)些困惑。

Lisp中的“變量"

很多教授Lisp的書(shū)中,大概會(huì)簡(jiǎn)單地告訴我們可以使用如下的方式定義一個(gè)全局變量 [1].

(defparameter *var* 1)

如上代碼,我們便定義了一個(gè)全局變量 *var*[2] ,它被初始化為數(shù)值1。同樣,我們 還可以使用另一種基本相同的方式:

(defvar *var* 1)

除了全局變量,我們還可以定義局部變量。但局部變量的定義稍顯麻煩(卻可能是另一種 設(shè)計(jì)考慮)。定義局部變量需要使用一些宏,或者特殊運(yùn)算符,例如:

(let ((var 1))
(format t "~a" var))

好了,就這些了。Lisp中關(guān)于變量的細(xì)節(jié),也就這些。你甚至能用你在C/C++中的經(jīng)驗(yàn)來(lái)窺 探一切。但是,我們很快就看到了很多困惑的地方。

我遇到的第一個(gè)困惑的地方來(lái)源于函數(shù),那么等我講講函數(shù)再來(lái)分享下坎坷。

Lisp中的函數(shù)

Lisp中的函數(shù)絕對(duì)不復(fù)雜,你絕對(duì)不用擔(dān)心我在忽悠你 [3] 。作為一門(mén)函數(shù)式語(yǔ)言,其首要 任務(wù)就是加強(qiáng)函數(shù)這個(gè)東西在整個(gè)語(yǔ)言里的功能。如果你喜歡廣閱各種與你工作不相干的 技術(shù),你肯定已經(jīng)對(duì)很多函數(shù)式語(yǔ)言世界中的概念略有耳聞。例如閉包,以及first class type [4]

Lisp中的函數(shù)就是first class type。這什么意思呢?直白來(lái)說(shuō), Lisp中的函數(shù)和變量 沒(méi)什么區(qū)別,享有同等待遇 。進(jìn)一步來(lái)說(shuō),變量fn的值可以是數(shù)值1,也可以是字符串 "hello",甚至是某個(gè)函數(shù)。這其實(shí)就是C++程序員說(shuō)的functor。

Lisp中定義函數(shù)非常簡(jiǎn)單:

(defun add2 (x)
(+ 2 x))

這樣,我們就定義了一個(gè)名為add2,有1個(gè)參數(shù),1個(gè)返回值的函數(shù)。要調(diào)用該函數(shù)時(shí),只需 要 (add2 2) 即可。這簡(jiǎn)直和我們?cè)贚isp中完成一個(gè)加法一模一樣:(+ 2 3)

Lisp作為一門(mén)函數(shù)式語(yǔ)言,其函數(shù)也能作為另一個(gè)函數(shù)的參數(shù)和返回值 [5]

(defun apply-fn (fn x)
(funcall fn x))

apply-fn函數(shù)第一個(gè)參數(shù)是一個(gè)函數(shù),它使用funcall函數(shù)間接地調(diào)用fn指向的函數(shù)。作為 一個(gè)C++程序員,這簡(jiǎn)直太好理解了,這完全就是一個(gè)函數(shù)指針的語(yǔ)法糖嘛。于是,假設(shè)我 們要使用apply-fn來(lái)間接調(diào)用add2函數(shù):

(apply-fn add2 2) ;; wrong

可是這是不對(duì)的。我們需要通過(guò)另一個(gè)特殊操作符來(lái)完成這件事:

(apply-fn #'add2 2) ;; right

#'操作符用于將add2對(duì)應(yīng)的函數(shù)取出來(lái),這么說(shuō)當(dāng)然不大準(zhǔn)確。Again,作為一個(gè)C++程序員 ,這簡(jiǎn)直就是個(gè)取地址操作符&的語(yǔ)法糖嘛。好吧,這么理解起來(lái)似乎沒(méi)問(wèn)題了。

Lisp中能甚至能在任何地方定義一個(gè)函數(shù),例如我們創(chuàng)建一個(gè)函數(shù),該函數(shù)返回創(chuàng)建出來(lái)的 函數(shù),這是一個(gè)典型的講解什么是 閉包 的例子:

(defun get-add-n (n)
#' (lambda (x)
(+ x n)))

無(wú)論如何,get-add-n函數(shù)返回一個(gè)函數(shù),該函數(shù)是add2函數(shù)的泛型實(shí)現(xiàn)。它可以將你傳入 的參數(shù)加上n。這些代碼里使用了lambda表達(dá)式。lambda表達(dá)式直白來(lái)說(shuō),就是創(chuàng)建一個(gè)字 面上的函數(shù)。這又是什么意思呢?就像我們?cè)诖a中寫(xiě)出2,寫(xiě)出"hello"一樣,2就是個(gè)字 面上的數(shù)字,"hello"就是個(gè)字面上的字符串 [6]

那么,總而言之,通過(guò)lambda創(chuàng)建一個(gè)函數(shù)體,然后通過(guò)#'操作符即可得到一個(gè)函數(shù),雖然 沒(méi)有名字。有了以上知識(shí)后,Again and again,作為一個(gè)C++程序員,很快我們就能得到一 個(gè)程序:定義變量,用變量去保存一個(gè)函數(shù),然后通過(guò)這個(gè)變量來(lái)調(diào)用這個(gè)函數(shù)。這是多么 天經(jīng)地義的事,就像之前那個(gè)通過(guò)參數(shù)調(diào)用其指向的函數(shù)一樣:

;; wrong
(defvar fn #' (lambda (x) (+ x 2)))
(fn 3)

這樣的代碼是不對(duì)的,錯(cuò)誤發(fā)生于第二行,無(wú)論你使用的Lisp實(shí)現(xiàn)是哪種,大概會(huì)得到如下 的錯(cuò)誤信息:

"The function FN is undefined."

老實(shí)說(shuō),這已經(jīng)算是多么有跡可循的錯(cuò)誤提示了啊。將以上代碼和之前的apply-fn對(duì)比,是 多么得神似啊,可惜就是錯(cuò)的。這是我們遇到的第一個(gè)理解偏差導(dǎo)致的問(wèn)題。如果你還不深 入探究,你將會(huì)在這一塊遇到更多麻煩。及時(shí)地拿出你的勇氣,披荊斬棘,刨根究底,絕對(duì) 是學(xué)習(xí)編程的好品質(zhì)。

“萬(wàn)惡之源“:SYMBOL

上文中提到的變量函數(shù)之類(lèi),之所以會(huì)在某些時(shí)候與我們的理解發(fā)生偏差,并且總是存在些 神秘的地方無(wú)法解釋。這完全是因?yàn)槲覀兝斫獾锰鎸?dǎo)致。Lisp中的Symbol可以說(shuō)就是某 個(gè)變量,或者某個(gè)函數(shù),但這太片面。Lisp中的Symbol擁有更豐富的含義。

Symbol的名字

就像很多語(yǔ)言的變量、函數(shù)名一樣,Lisp中的Symbol比其他語(yǔ)言在命名方面更自由: 只 要位于'|'字符之間的字符串,就表示一個(gè)合法的Symbol名。 我們可以使用函數(shù) symbol-name來(lái)獲取一個(gè)Symbol的名字,例如:

(symbol-name '|this is a symbol name|)
輸出:"this is a symbol name"

'(quote)操作符告訴Lisp不要對(duì)其修飾的東西進(jìn)行求值(evaluate)。但假如沒(méi)有這個(gè)操作符 會(huì)怎樣呢?后面我們將看到會(huì)怎樣。

Symbol本質(zhì)

<ANSI Common Lisp>一書(shū)中有句話真正地揭示了Symbol的本質(zhì): Symbols are real objects 。是的,Symbols是對(duì)象,這個(gè)對(duì)象就像我們理解的C++中的對(duì)象一樣,它是一個(gè) 復(fù)合的數(shù)據(jù)結(jié)構(gòu)。該數(shù)據(jù)結(jié)構(gòu)里包含若干域,或者通俗而言:數(shù)據(jù)成員。借用<ANSI Common Lisp>中的一圖:

imgs/symbol-obj.png

通過(guò)這幅圖,可以揭開(kāi)所有謎底。一個(gè)Symbol包含至少圖中的幾個(gè)域,例如Name、Value、 Function等。在Lisp中有很多函數(shù)來(lái)訪問(wèn)這些域,例如上文中使用到的symbol-name,這個(gè) 函數(shù)本質(zhì)上就是取出一個(gè)Symbol的Name域。

Symbol與Variable和Function的聯(lián)系

自然而然地,翻閱Lisp文檔,我們會(huì)發(fā)現(xiàn)果然還有其他函數(shù)來(lái)訪問(wèn)Symbol的其他域,例如:

symbol-function
symbol-value
symbol-package
symbol-plist

但是這些又與上文提到的變量和函數(shù)有什么聯(lián)系呢?真相只有一個(gè), 變量、函數(shù)粗略來(lái) 說(shuō)就是Symbol的一個(gè)域,一個(gè)成員。變量對(duì)應(yīng)Value域,函數(shù)對(duì)應(yīng)Function域。一個(gè)Symbol 這些域有數(shù)據(jù)了,我們說(shuō)它們發(fā)生了綁定(bind)。 而恰好,我們有幾個(gè)函數(shù)可以用于判 定這些域是否被綁定了值:

boundp ;判定Value域是否被綁定
fboundp;判定Function域是否被綁定

通過(guò)一些代碼來(lái)回味以上結(jié)論:

(defvar *var* 1)
(boundp '*var*) ; 返回真
(fboundp '*var*) ; 返回假
(defun *var* (x) x) ; 定義一個(gè)名為*var*的函數(shù),返回值即為參數(shù)
(fboundp '*var*) ; 返回真

上面的代碼簡(jiǎn)直揭秘了若干驚天地泣鬼神的真相。首先,我們使用我們熟知的defvar定義了 一個(gè)名為 *var* 的變量,初值為1,然后使用boundp去判定 *var* 的Value域是否 發(fā)生了綁定。這其實(shí)是說(shuō): 原來(lái)定義變量就是定義了一個(gè)Symbol,給變量賦值,原來(lái)就 是給Symbol的Value域賦值!

其實(shí),Lisp中所有這些符號(hào),都是Symbol。 什么變量,什么函數(shù),都是浮云。上面的 例子中,緊接著用fboundp判斷Symbol *var* 的Function域是否綁定,這個(gè)時(shí)候?yàn)榧佟? 然后我們定義了一個(gè)名為 *var* 的函數(shù),之后再判斷,則已然為真。這也是為什么, 在Lisp中某個(gè)函數(shù)可以和某個(gè)變量同名的原因所在。 從這段代碼中我們也可以看出 defvar/defun這些操作符、宏所做事情的本質(zhì)。

More More More

事情就這樣結(jié)束了?Of course not。還有很多上文提到的疑惑沒(méi)有解決。首先,Symbol是 如此復(fù)雜,那么Lisp如何決定它在不同環(huán)境下的含義呢?Symbol雖然是個(gè)對(duì)象,但它并不像 C++中的對(duì)象一樣,它出現(xiàn)時(shí)并不指代自己!不同應(yīng)用環(huán)境下,它指代的東西也不一樣。這 些指代主要包括變量和函數(shù),意思是說(shuō): Symbol出現(xiàn)時(shí),要么指的是它的Value,要么是 它的Function。 這種背地里干的事情,也算是造成迷惑的一個(gè)原因。

當(dāng)一個(gè)Symbol出現(xiàn)在一個(gè)List的第一個(gè)元素時(shí),它被處理為函數(shù)。這么說(shuō)有點(diǎn)迷惑人,因?yàn)? 它帶進(jìn)了Lisp中代碼和數(shù)據(jù)之間的模糊邊界特性。簡(jiǎn)單來(lái)說(shuō),就是當(dāng)Symbol出現(xiàn)在一個(gè)括號(hào) 表達(dá)式(s-expression)中第一個(gè)位置時(shí),算是個(gè)函數(shù),例如:

(add2 3) ; add2位于第一個(gè)位置,被當(dāng)作函數(shù)處理
(*var* 3) ; 這里*var*被當(dāng)作函數(shù)調(diào)用,返回3

除此之外,我能想到的其他大部分情況,一個(gè)Symbol都被指代為它的Value域,也就是被當(dāng) 作變量,例如:

(*var* *var*) ; 這是正確的語(yǔ)句,返回1

這看起來(lái)是多么古怪的代碼。但是運(yùn)用我們上面說(shuō)的結(jié)論,便可輕易解釋?zhuān)罕磉_(dá)式中第一個(gè) *var* 被當(dāng)作函數(shù)處理,它需要一個(gè)參數(shù);表達(dá)式第二部分的 *var* 被當(dāng)作變量 處理,它的值為1,然后將其作為參數(shù)傳入。

再來(lái)說(shuō)說(shuō)'(quote)操作符,這個(gè)操作符用于防止其操作數(shù)被求值。而當(dāng)一個(gè)Symbol出現(xiàn)時(shí), 它總是會(huì)被求值,所以,我們可以分析以下代碼:

(symbol-value *var*) ; wrong

這個(gè)代碼并不正確,因?yàn)?*var* 總是會(huì)被求值,就像 (*var* *var*) 一樣,第二 個(gè) *var* 被求值,得到數(shù)字1。這里也會(huì)發(fā)生這種事情,那么最終就等同于:

(symbol-value 1) ; wrong

我們?cè)噲D去取數(shù)字1的Value域,而數(shù)字1并不是一個(gè)Symbol。所以,我們需要quote運(yùn)算符:

(symbol-value '*var*) ; right

這句代碼是說(shuō),取Symbol *var* 本身的Value域!而不是其他什么地方。至此,我們 便可以分析以下復(fù)雜情況:

(defvar *name* "kevin lynx")
(defvar *ref* '*name*) ; *ref*的Value保存的是另一個(gè)Symbol
(symbol-value *ref*) ; 取*ref*的Value,得到*name*,再取*name*的Value

現(xiàn)在,我們甚至能解釋上文留下的一個(gè)問(wèn)題:

;; wrong
(defvar fn #' (lambda (x) (+ x 2)))
(fn 3)

給fn的Value賦值一個(gè)函數(shù), (fn 3) 當(dāng)一個(gè)Symbol作為函數(shù)使用時(shí),也就是取其 Function域來(lái)做調(diào)用。但其Function域什么也沒(méi)有,我們?cè)噲D將一個(gè)Symbol的Value域當(dāng)作 Function來(lái)使用。如何解決這個(gè)問(wèn)題?想想,symbol-function可以取到一個(gè)Symbol的 Function域:

(setf (symbol-function 'fn) #' (lambda (x) (+ x 2)))
(fn 3)

通過(guò)顯示地給fn的Function域賦值,而不是通過(guò)defvar隱式地對(duì)其Value域賦值,就可以使 (fn 3) 調(diào)用正確。還有另一個(gè)問(wèn)題也能輕易解釋:

(apply-fn add2 2) ; wrong

本意是想傳入add2這個(gè)Symbol的function域,但是直接這樣寫(xiě)的話,傳入的其實(shí)是add2的 Value域 [7] ,這當(dāng)然是不正確的。對(duì)比正確的寫(xiě)法,我們甚至能猜測(cè)#'運(yùn)算符就是一個(gè) 取Symbol的Function域的運(yùn)算符。進(jìn)一步,我們還可以給出另一種寫(xiě)法:

(apply-fn (symbol-function 'add2) 2)

深入理解事情的背后,你會(huì)發(fā)現(xiàn)你能寫(xiě)出多么靈活的代碼。

END

關(guān)于Symbol的內(nèi)容還有更多,例如Package。正確理解這些內(nèi)容以及他們之間的關(guān)系,有助 于更深刻地理解Lisp。

注解

[1] 在Lisp中全局變量又被稱(chēng)為dynamic variables
[2] Lisp中按照習(xí)慣通常在為全局變量命名時(shí)會(huì)加上星號(hào),就像我們習(xí)慣使用g_一樣
[3] 因?yàn)槲掖_實(shí)在忽悠你
[4] first class type,有人翻譯為“一等公民”,我覺(jué)得壓力巨大
[5] 即高階函數(shù)
[6] “字面“主要是針對(duì)這些信息會(huì)被詞法分析程序直接處理
[7] 這可能導(dǎo)致更多的錯(cuò)誤

posted @ 2011-03-22 11:33 Kevin Lynx 閱讀(5249) | 評(píng)論 (8)編輯 收藏

用lisp開(kāi)發(fā)博客客戶端

用lisp開(kāi)發(fā)博客客戶端

Author: Kevin Lynx
Date: 3.13.2011

最近一直在學(xué)習(xí)Lisp這門(mén)語(yǔ)言。回頭一看,基本上接近1個(gè)月了。剛開(kāi)始接觸Lisp是因?yàn)榭? 了<Lisp本質(zhì)>,然后我發(fā)現(xiàn)有很多人宗教般地忠誠(chéng)這門(mén)語(yǔ)言,于是就來(lái)了興趣。

imgs/lisp_believer.png

當(dāng)然并不是每次因?yàn)槟称獙?xiě)得很geek技術(shù)文章就去學(xué)習(xí)某個(gè)新的技術(shù)點(diǎn)。一個(gè)月時(shí)間對(duì)我來(lái) 說(shuō)還是很珍貴了。但是Lisp絕對(duì)是大部分程序員都值得一學(xué)的語(yǔ)言(就像Haskell一樣)。 我能給出的簡(jiǎn)單理由包括:

  • 大部分程序員只會(huì)命令式語(yǔ)言(C/C++/C Like etc),缺乏函數(shù)式語(yǔ)言解決編程問(wèn)題的思 想(當(dāng)然Lisp不是純函數(shù)式)
  • Lisp是僅次于Fortran的古老語(yǔ)言,很多優(yōu)秀的語(yǔ)言設(shè)計(jì)思想在現(xiàn)代的一些語(yǔ)言里都找得 到
  • 裝B黨必備

另一方面,結(jié)合我一個(gè)月以來(lái)的讀書(shū)和兩個(gè)練習(xí)工程的實(shí)踐經(jīng)歷,我覺(jué)得也有些理由值得你 不去學(xué)習(xí)Lisp:

  • 你會(huì)Haskell或者其他函數(shù)式語(yǔ)言
  • 我目前還是覺(jué)得Lisp學(xué)習(xí)曲線高(大概是因?yàn)槲易x到的書(shū)都在應(yīng)用語(yǔ)法層兜圈子,事實(shí)上 Lisp的語(yǔ)法之統(tǒng)一,全特么的是s-expression),你不愿意花費(fèi)這些成本
  • you are too old bo to be a B

關(guān)于這篇文檔

這篇博客我使用reStructuredText格式編寫(xiě),然后用docutls導(dǎo)出為html,再然后使用這回 用lisp開(kāi)發(fā)的基于metaweblog API的博客客戶端,自動(dòng)發(fā)布到CPPBLOG。

他們?cè)趺凑f(shuō)Lisp

我就摘錄些書(shū)上的觀點(diǎn)(歷史):

  • 1958年,John McCarthy和他的學(xué)生搞出了Lisp,包括其第一個(gè)實(shí)現(xiàn),最初貌似也是以一 篇論文起頭
  • Lisp可以讓你做其他語(yǔ)言里無(wú)法做的事情(<ANSI common Lisp>)
  • 大部分編程語(yǔ)言只會(huì)告訴你不能怎樣做,這限制了你解決問(wèn)題的思路,Lisp not (<ANSI Common Lisp>)
  • Lisp讓你以Lisp的思維思考問(wèn)題,換到其他語(yǔ)言你會(huì)說(shuō):為什么X語(yǔ)言就不支持這個(gè)特性 呢(Once you've leanred Lisp, you'll even dream in Lisp) (<Land Of Lisp>)
  • Lisp代碼更清晰地體現(xiàn)你的想法(<Practical Common Lisp>)

And my opinion

我可還沒(méi)到把Lisp捧上天的地步。如果Lisp如此之好,為什么用的人不多?<Land Of Lisp> 里作者恰好對(duì)這個(gè)問(wèn)題做了回答(bla bla bla,懶得細(xì)讀)。

  • Lisp也是一門(mén)雜和型風(fēng)格的語(yǔ)言,函數(shù)式、命令式、面向?qū)ο螅约白畋蝗舜蹬醯暮昃幊? --程序自己寫(xiě)自己
  • Lisp的語(yǔ)句全部以(xxx xxx)的形式出現(xiàn),被稱(chēng)為s-expression,我看稱(chēng)為括號(hào)表達(dá)式還 差不多
  • Lisp每條語(yǔ)句都有返回值,沒(méi)基礎(chǔ)過(guò)函數(shù)式編程的同學(xué),if語(yǔ)句也是有返回值的
  • 函數(shù)式編程語(yǔ)言的一個(gè)重要特性就是閉包(closure),這個(gè)東西用來(lái)避免全局變量實(shí)在太 geek了

開(kāi)始學(xué)習(xí)Lisp

Lisp不像有些語(yǔ)言,有個(gè)直接的機(jī)構(gòu)來(lái)維護(hù)。感覺(jué)它更像C/C++一樣,只有個(gè)標(biāo)準(zhǔn),然后有 若干編譯器(解釋器)實(shí)現(xiàn)。Lisp在幾十年的發(fā)展中,產(chǎn)生了很多種方言。方言也就是形變 神不變的語(yǔ)言變種,本文說(shuō)的Lisp均指Lisp的方言Common Lisp。另一個(gè)比較有名的方言是 Scheme,關(guān)于各個(gè)方言的特點(diǎn),<Land Of Lisp>里也給了一個(gè)圖片:

imgs/dialect.png

其中,最左邊那只wolf就是Common Lisp,右邊那只sheep就是Scheme。

要學(xué)習(xí)Lisp,首先就是選擇方言。然后最重要的就是選擇一個(gè)編譯器實(shí)現(xiàn)。世界上知名的有 十幾種實(shí)現(xiàn)(也許更多)。一些商業(yè)版本非常強(qiáng)大,甚至能編譯出很小的本地代碼執(zhí)行文件 ,不過(guò)價(jià)格也不菲。當(dāng)然也有很多開(kāi)源免費(fèi)的實(shí)現(xiàn),例如CLISP、SBCL。我選用的是SBCL。

SBCL交互式命令行不支持括號(hào)匹配,甚至沒(méi)有輸入歷史。要實(shí)現(xiàn)這兩個(gè)功能,可以裝一個(gè) lisp工具:linedit。在lisp的世界中,要獲得一個(gè)lisp的庫(kù)實(shí)在不是件方便的事。尤其是 這些免費(fèi)的編譯器實(shí)現(xiàn),并不像有些語(yǔ)言一樣,直接隨編譯器帶個(gè)幾十M的庫(kù)。

然后就有了quicklisp這個(gè)工具。該工具就像Ubuntu系統(tǒng)里的軟件管理器一樣,你可以在 lisp里直接獲取某個(gè)庫(kù)。quicklisp檢查該庫(kù)是否存在,不存在直接從它的服務(wù)器上下載人 然后自動(dòng)安裝。

此外,在lisp的世界里,寫(xiě)出來(lái)的程序不再是跨OS。OS的差異由編譯器實(shí)現(xiàn)來(lái)解決。但是, 寫(xiě)lisp程序卻需要考慮跨編譯器實(shí)現(xiàn)(egg hurt)。這也是個(gè)無(wú)比傷神的事,比跨OS更傷 神。因?yàn)镺S就那么幾個(gè),但lisp的編譯器實(shí)現(xiàn),流行的也有好幾個(gè)。

lisp的世界里,工程組織也有特殊的一套,就像makefile一樣,這就是asdf。

博客客戶端如何實(shí)現(xiàn)

像我們這種基本沒(méi)接觸過(guò)Web開(kāi)發(fā)的人,可能完全沒(méi)有思路去實(shí)現(xiàn)一個(gè)博客客戶端。事實(shí)上 實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單。

使用過(guò)其他博客客戶端(例如Windows Live writer)的人肯定知道m(xù)etaweblog API,在配 置客戶端的時(shí)候需要填入。例如CPPBLOG的這個(gè)地址就是 http://m.shnenglu.com/kevinlynx/services/metaweblog.aspx。這個(gè)頁(yè)面展示了一些API 說(shuō)明。這些API就是博客客戶端和服務(wù)器進(jìn)行操作通信的接口。意思是說(shuō),服務(wù)器端提供這 這些接口,我們的客戶端調(diào)用這些接口即可。例如:

blogger.deletePost,調(diào)用該接口即可刪除一篇博客文章

但是客戶端如何調(diào)用到這個(gè)接口呢?這需要通過(guò)一種新的技術(shù)(或者說(shuō)標(biāo)準(zhǔn)),即 xml rpc 。rpc大家應(yīng)該清楚,xml rpc其實(shí)說(shuō)白了, 就是把接口調(diào)用的細(xì)則塞進(jìn) http 請(qǐng)求發(fā)給web服務(wù)器,服務(wù)器接收請(qǐng)求完成操作后再把結(jié)果以http回應(yīng)的形式丟給客戶端, 即完成了一次接口調(diào)用

至于http請(qǐng)求回應(yīng)的細(xì)則就不提了,無(wú)非就是一些特殊格式的數(shù)據(jù),通過(guò)tcp連接與服務(wù)器 交互這些數(shù)據(jù)。

所以,基本上,整個(gè)過(guò)程還是非常簡(jiǎn)單。如何來(lái)將調(diào)用細(xì)節(jié)塞進(jìn)http請(qǐng)求,則是以xml rpc 標(biāo)準(zhǔn)來(lái)做,其格式正好是xml格式。舉個(gè)例子吧:

<?xml version='1.0'?>
<methodCall>
    <methodName>title_or_id</methodName>
        <params>
        </params>
</methodCall

當(dāng)然這部分?jǐn)?shù)據(jù)之前就是若干http請(qǐng)求的數(shù)據(jù)。服務(wù)器回應(yīng)也是以xml格式組織:

<?xml version='1.0'?>
<methodResponse>
    <params>
        <param>
            <value><string>Welcome to Zope.org</string></value>
        </param>
    </params>
</methodResponse>

我們的博客客戶端所要做的,就是把這些博客發(fā)布相關(guān)的操作封裝起來(lái)提供給使用者。底層 實(shí)現(xiàn)主要包括http請(qǐng)求、xml-rpc的組織等。何況,這兩部分在各個(gè)語(yǔ)言里都有大量的庫(kù)存 在,lisp自然也有。

我這里直接選取了lisp的一個(gè)xml-rpc庫(kù):s-xml-rpc,基本上百來(lái)行代碼就可以把各個(gè)功 能跑一遍。例如以下lisp代碼就實(shí)現(xiàn)了通過(guò)s-xml-rpc刪除CPPBLOG的一篇文章:

(defun delete-post (postid)
  (rpc-call
    "blogger.deletePost"
    postid
    "kevinlynx"
    "password"
    t))

發(fā)布博客也很簡(jiǎn)單,根據(jù)metaweblog API接口的說(shuō)明,發(fā)布博客時(shí)需要填充一個(gè)結(jié)構(gòu)體。但 主要涉及到的數(shù)據(jù)僅包括:文章內(nèi)容、文章標(biāo)題、文章分類(lèi)(可選):

(defun new-post (title context &optional (cates))
  (rpc-call
    "metaWeblog.newPost"
    ""
    "kevinlynx"
    "password"
    (new-post-struct title context cates)
    t))

值得注意的是,如果文章中有貼圖,則需要事先將圖片文件上傳到服務(wù)器。CPPBLOG的 metaweblog API里恰有API提供:

(defun new-media-object (filename)
  (rpc-call
    "metaWeblog.newMediaObject"
    ""
    "kevinlynx"
    "password"
    (new-media-object-struct filename)))

該函數(shù)讀入圖片文件,然后調(diào)用metaWeblog.newMediaObject接口,即可完成上傳。上傳成 功后,服務(wù)器會(huì)返回該圖片的URL。然后在我們的文章中就可以使用該圖片了。

完整實(shí)現(xiàn)方案

僅僅將metaweblog的一些接口做封裝,對(duì)于一個(gè)可以使用的博客客戶端來(lái)說(shuō)還遠(yuǎn)遠(yuǎn)不夠。大 部分同類(lèi)工具都有一個(gè)友好的GUI編輯界面。我并不打算弄一個(gè)編輯界面出來(lái),吃力不討好 的事情。

我的打算是先用其他工具對(duì)文章做排版處理,最后導(dǎo)出為html格式。因?yàn)镃PPBLOG支持直接 發(fā)布一個(gè)html文件。然后在用這個(gè)lisp工具將整個(gè)文件作為博客文章內(nèi)容發(fā)布。

恰好公司最近打算用reStructureText(rst)格式來(lái)編輯文檔,作為熟悉手段,我決定拿這個(gè) 來(lái)練手。rst格式非常簡(jiǎn)單,同wiki命令很相似。在vim里編輯該文件非常合適,因?yàn)槟J(rèn)支 持。見(jiàn)圖:

imgs/rst.png

由圖即可看出,rst是一種半所見(jiàn)即所得的格式。即:它遵循你在編輯器里的排版,同時(shí)也 通過(guò)一些tag(例如image)來(lái)控制更豐富的輸出。

rst有很多前端工具,可以將rst文件輸出,例如rst2html.py就可以輸出為html。好吧,最 最終我們得到了html格式的博客文章。

但是如果文章中出現(xiàn)了圖片,而圖片基本上在本地,轉(zhuǎn)成html后也是相對(duì)路徑。我需要我的 lisp writer(cl-writer)能自動(dòng)掃描文章,發(fā)現(xiàn)有圖片的地方,就自動(dòng)將圖片上傳。最?lèi)盒? 的是上傳后還得替換圖片引用路徑。這個(gè)工作可以在rst格式上做,也可以在結(jié)果格式html 上做。通過(guò)xml解析庫(kù)解析html比直接解析rst格式更簡(jiǎn)單,并且在擴(kuò)展性上更好。

最終這個(gè)html中圖片路徑替換工作只消耗了不到100行l(wèi)isp代碼。這在很大程度上也依賴于 s-xml庫(kù)的接口設(shè)計(jì)。

最終封裝好的發(fā)布接口如下,從這里也可以看出,函數(shù)式語(yǔ)言鍛煉我們寫(xiě)出功能單一代碼度 短小的接口:

(defun writer-post-new (post-file &key (u (get-default-user))(cates))
  (read-post-file u post-file context title
                  (new-post u title context cates)))

END

別指望我發(fā)布的代碼能夠讓你一鍵在你的博客上留下"this is a test",你甚至別指望它能 能夠工作。但如果你本來(lái)就是一個(gè)資深的lisper,或者雖然不是lisper但卻執(zhí)意想看看結(jié)果 。這里我就簡(jiǎn)要說(shuō)說(shuō)如何讓這些代碼歡樂(lè)起來(lái):

  1. OS Ubuntu10.04,下載安裝SBCL,不會(huì)有問(wèn)題;

  2. 下載安裝quicklisp,官方文檔hand by hand,簡(jiǎn)單不會(huì)有問(wèn)題;

  3. SBCL交互環(huán)境中使用quicklisp安裝s-xml-rpc:

    (ql:quickload "s-xml-rpc")
    
  4. 裝載我的代碼:

    (asdf:load-system :cl-writer)
    
  5. 在home下添加配置文件.cl-writer.lisp,配置你博客信息,例如:

    (in-package cl-writer)
    (setf *default-user* (make-cppblog-user "賬戶名" "密碼"))
    

    如果你的博客不在CPPBLOG,雖然也許也是metaweblog,但我不能保證成功,配置文件則 要復(fù)雜點(diǎn):

    (setf *default-user* (make-user-info :name "帳戶名"
                          :password "密碼" :host "m.shnenglu.com"
                          :url "/kevinlynx/services/metaweblog.aspx"))
    
  6. SBCL交互環(huán)境下測(cè)試:

    (in-package cl-writer)
    (new-post (get-default-user) "this is a test" "title")
    

下載代碼

最后,終于敲完這篇文章,我需要通過(guò)以下步驟來(lái)發(fā)表它:

in shell:
rst2html.py lisp_xml_rpc.rst lisp_xml_rpc.html
in SBCL:
(writer-post-new "lisp_xml_rpc.html")

;;EOF;;

posted @ 2011-03-13 13:19 Kevin Lynx 閱讀(16907) | 評(píng)論 (8)編輯 收藏

飛秋lua版:luafeiq0.1.0發(fā)布

繼上次搗鼓出了飛秋的群聊協(xié)議后,鑒于年底沒(méi)啥事情做,就用lua寫(xiě)了個(gè)簡(jiǎn)單的協(xié)議兼容的IM。本來(lái)開(kāi)始讓

另一個(gè)同事在iptux的基礎(chǔ)上修改的,結(jié)果大概是因?yàn)閕ptux的代碼不是那么容易修改,就不了了之了。這個(gè)

剛發(fā)布的luafeiq功能非常簡(jiǎn)單,僅支持與飛秋(包括大部分兼容IP messager的IM)進(jìn)行單聊,群消息的

收發(fā),簡(jiǎn)易的消息盒子(暫存未讀消息)。因?yàn)檫x的庫(kù)都是跨平臺(tái)的,所以很容易的luafeiq也是跨平臺(tái)的,

最主要的是我想在linux下使用。

 

之所以選用lua,一方面是想練練lua,另一方面則是因?yàn)殚_(kāi)發(fā)效率。前段時(shí)間在android下寫(xiě)了些java代碼

用java寫(xiě)代碼覺(jué)得甚為爽快(當(dāng)然算不了完美)。這幾天寫(xiě)了千把行的lua(也許有3K行,未統(tǒng)計(jì)過(guò)),

感覺(jué)也不錯(cuò)。綜合來(lái)說(shuō),這些高級(jí)語(yǔ)言的很多好用的語(yǔ)法特性,例如閉包(closure),垃圾回收,都提高

了不少寫(xiě)代碼的速度。當(dāng)然,lua于我而言也算不上完美的語(yǔ)言。例如我經(jīng)常因?yàn)樽兞壳缅e(cuò)字母,而在運(yùn)行時(shí)

才暴露nil錯(cuò)誤。這也許可以通過(guò)諸如IDE之類(lèi)的工具在寫(xiě)代碼的時(shí)候就給予提示。lua 在遇到一個(gè)符號(hào)時(shí),

默認(rèn)地將其處理為全局的。關(guān)于這個(gè)語(yǔ)法特性早有人提出不爽,只能說(shuō)大家設(shè)計(jì)的準(zhǔn)則不一樣。(在我們

項(xiàng)目里,我直接改寫(xiě)了全局變量的metatable,從而防止策劃隨意定義全局變量)

 

再來(lái)談?wù)剬?shí)現(xiàn)過(guò)程中的一些瑣事。因?yàn)轱w秋也算是IP messager協(xié)議的兼容實(shí)現(xiàn),很多通信除了可以使用

抓包軟件分析外,還可以直接通過(guò)IP messager的源碼來(lái)了解。所以,基礎(chǔ)通信協(xié)議的實(shí)現(xiàn)過(guò)程也比較

簡(jiǎn)單。飛秋與飛秋之間發(fā)送私聊消息是經(jīng)過(guò)加密的。其加密過(guò)程也不簡(jiǎn)單,更重要的是,我并不想浪費(fèi)太

多時(shí)間在這上面。后來(lái)發(fā)現(xiàn)其實(shí)可以通過(guò)上線消息里某個(gè)標(biāo)志位表明自己不需要加密。這個(gè)標(biāo)志就是消息頭

里的option。上線廣播出去的消息里一旦表明自己不加密,那么以后和飛秋通信也就不需要解密了。

 

發(fā)送私聊消息時(shí),消息里會(huì)攜帶一個(gè)消息ID。這個(gè)ID可以通過(guò)任意算法生成,例如直接取time的值。接收到

對(duì)方的消息時(shí),需要取出該ID,然后加入回應(yīng)消息。對(duì)方收到回應(yīng)消息后,就知道自己發(fā)送成功。這個(gè)過(guò)程

算是ip messager在UDP上做的消息可靠驗(yàn)證,過(guò)程也比較簡(jiǎn)單。

 

群聊消息在之前提到過(guò),是通過(guò)UDP多播實(shí)現(xiàn)。我們可以接收所有群的消息。如果之前已經(jīng)處于某個(gè)群里,

那么一旦你上線后(廣播上線消息),你就可以直接在這個(gè)群里發(fā)言。但如果你之前不在這個(gè)群里,則

可以通過(guò)多播一個(gè)加入群的消息,然后就可以不請(qǐng)自來(lái)地在這個(gè)群里發(fā)言。詳細(xì)的消息值和實(shí)現(xiàn)都可以從

luafeiq的代碼里讀到(message_sender.lua)。

 

在linux下接收windows上的飛秋消息,是需要做字符編碼轉(zhuǎn)換的。因?yàn)閘uafeiq使用IUP作為UI庫(kù),IUP在

linux下使用GTK作為底層實(shí)現(xiàn),默認(rèn)全部是UTF8編碼。luafeiq里我自己寫(xiě)了個(gè)lua庫(kù),用于編碼轉(zhuǎn)換。

 

話說(shuō)IUP作為一個(gè)UI庫(kù),還是比較不錯(cuò)的。正如其介紹文檔里所說(shuō),學(xué)習(xí)曲線低,基本上看一會(huì)文檔,就可以

直接使用了。luafeiq使用的IUP版本至少需要3.0以上。當(dāng)初在linux下為了安裝IUP3.3,基本花了4個(gè)小時(shí)

時(shí)間,各種奇怪的沒(méi)多大意義的錯(cuò)誤信息。后來(lái)?yè)Q成3.2版本,居然一下子就和諧了,無(wú)限怨念。

 

luafeiq目前放在googlecode的版本,可以說(shuō)是一個(gè)很不負(fù)責(zé)任的版本。早上我才剛把字符編碼轉(zhuǎn)換的代碼

調(diào)試好。今天已經(jīng)請(qǐng)假,家里就一臺(tái)電腦,也就測(cè)試不了這個(gè)字符編碼轉(zhuǎn)換是否真的能正常工作。我在

windows下dump了些字符,看上去能正常功能。明天得回老家過(guò)春節(jié),上不了網(wǎng),索性就提前發(fā)布了。

 

luafeiq項(xiàng)目地址:http://code.google.com/p/luafeiq/

posted @ 2011-01-31 16:51 Kevin Lynx 閱讀(4586) | 評(píng)論 (2)編輯 收藏

逆向思路:破解飛秋群聊協(xié)議

題外

飛秋是一款局域網(wǎng)內(nèi)的IM軟件,界面類(lèi)似QQ,實(shí)現(xiàn)上與飛鴿(IP message)有點(diǎn)淵源,免費(fèi),不開(kāi)源。

公司大概兩年前開(kāi)始使用這款軟件作為員工之間辦公吹牛的工具。最近游戲玩得少,就想徹底換到linux下,

組內(nèi)也有其他兩人是llinux-er,不過(guò)悲劇的是換到linux下就無(wú)法收取飛秋群里的聊天信息了,不免寂寞。

所以,就想寫(xiě)個(gè)協(xié)議兼容的程序(或者說(shuō)改個(gè)東西)來(lái)收取群信息。

 

準(zhǔn)備

我本身并不擅長(zhǎng)逆向工程,破解什么的純碎業(yè)余。在GOOGLE/BAIDU之后發(fā)現(xiàn)并沒(méi)有前人留下的成果。

使用抓包程序,以及綜合網(wǎng)絡(luò)上的信息,還是可以得出不少有用的信息:

# 飛秋兼容了飛鴿的協(xié)議,其協(xié)議格式基本上基于文本形式,各個(gè)內(nèi)容之間使用冒號(hào)作為分隔,例如:

1:100:user:pcname:32:message_body

# 飛秋在私聊情況下的消息內(nèi)容是沒(méi)有加密的,但群聊信息加密了,解密這個(gè)內(nèi)容也是我的目標(biāo)所在

# 在抓包軟件下根據(jù)消息的目標(biāo)IP地址可以推斷飛秋發(fā)送群信息是基于UDP組播的,即就算你不是這個(gè)群

   的成員,也會(huì)收到群消息

有用的文章: 《局域網(wǎng)內(nèi)實(shí)現(xiàn)飛鴿欺騙》《飛鴿傳書(shū)數(shù)據(jù)加密分析》(個(gè)人感覺(jué)沒(méi)有任何實(shí)質(zhì)內(nèi)容,而

且飛鴿傳書(shū)并不是飛秋,屬于誤導(dǎo)性文章,但是依然有用)。最重要的,稍微瀏覽IP message的代碼,

以及l(fā)inux下的iptux(另一個(gè)兼容飛鴿的局域網(wǎng)IM)代碼,都是對(duì)接下來(lái)的破解有益的。

 

破解

我希望我提供更多的,是一種crack的思路,雖然我不是一個(gè)cracker。破解和寫(xiě)程序不太一樣,它需要

更多的耐心、運(yùn)氣、程序之外的思考。如前所做的準(zhǔn)備,尤其重要。

工具及環(huán)境:飛秋2.4版本、OllyDbg,為了方便接收群信息,最好有兩臺(tái)電腦。

 

STEP 1 找入手點(diǎn)

在開(kāi)始面對(duì)一大推匯編代碼時(shí),我們需要一個(gè)最接近目標(biāo)的點(diǎn)。獲取這個(gè)點(diǎn)根據(jù)目標(biāo)的不同而不同。例如,

這里主要是針對(duì)網(wǎng)絡(luò)數(shù)據(jù)的解密。所以,最直接的就是去找這些網(wǎng)絡(luò)數(shù)據(jù)。當(dāng)然,根據(jù)具體程序的表現(xiàn),也

可以從其他點(diǎn)入手,例如飛秋收到群消息后會(huì)在任務(wù)欄閃爍圖標(biāo),也可以從這個(gè)功能逆向過(guò)去。

 

因?yàn)轱w秋使用UDP協(xié)議,所以我們可以在recvfrom函數(shù)下斷點(diǎn)(bp recvfrom)。因?yàn)榻邮誙DP包的接口

可能還有WSARecvFrom,甚至winsock1.0中的recvfrom,所以最好都下斷點(diǎn)。另一臺(tái)機(jī)器發(fā)送群消息,

程序在winsock1.0里的recvfrom斷下來(lái):

71A43001 >  8BFF            mov     edi, edi
71A43003    55              push    ebp
71A43004    8BEC            mov     ebp, esp
71A43006    51              push    ecx

這個(gè)不是我們需要的,我們需要根據(jù)這個(gè)點(diǎn)獲得用戶層代碼,這將是整個(gè)破解過(guò)程的起點(diǎn)。所以,OD中

ALT+K查看調(diào)用堆棧,然后跳到調(diào)用recvfrom的函數(shù)處:

00490890  /$  56            push    esi                              ;  接收數(shù)據(jù)的函數(shù)入口
00490891  |.  8B7424 08     mov     esi, dword ptr [esp+8]
00490895  |.  8D46 10       lea     eax, dword ptr [esi+10]
00490898  |.  50            push    eax                              ; /pFromLen
00490899  |.  56            push    esi                              ; |pFrom
0049089A  |.  C700 10000000 mov     dword ptr [eax], 10              ; |
004908A0  |.  8B09          mov     ecx, dword ptr [ecx]             ; |
004908A2  |.  6A 00         push    0                                ; |Flags = 0
004908A4  |.  8D46 18       lea     eax, dword ptr [esi+18]          ; |
004908A7  |.  68 FF3F0000   push    3FFF                             ; |BufSize = 3FFF (16383.)
004908AC  |.  50            push    eax                              ; |Buffer
004908AD  |.  51            push    ecx                              ; |Socket
004908AE  |.  E8 C7F30C00   call    <jmp.&WSOCK32.#17>               ; \recvfrom

邪惡的OD已經(jīng)將調(diào)用recvfrom時(shí)傳入?yún)?shù)的指令標(biāo)記出來(lái)了。中文注釋是我分析時(shí)加的。recvfrom里傳入

的接收緩存,是我們應(yīng)該關(guān)注的。如果能跟進(jìn)這個(gè)緩存,假設(shè)程序的流程比較簡(jiǎn)單:接收了數(shù)據(jù),然后在某個(gè)

地方直接解密,不管它的加密方式是什么,只要能找到這個(gè)緩存數(shù)據(jù)從加密變?yōu)榻饷艿牡胤剑瑢?duì)于整個(gè)破解而言,

都算是邁進(jìn)了一大步。

于是,在00490890(上面找到的函數(shù)入口)下斷點(diǎn),準(zhǔn)備跟進(jìn)接收緩存。注意:在OD里調(diào)試跟在vc里不一樣,

跳到調(diào)用堆棧里的某個(gè)函數(shù),寄存器依然是當(dāng)前的值。所以需要重新跟。

 

STEP 2 內(nèi)存斷點(diǎn)

F9讓程序繼續(xù)運(yùn)行,再次在另一臺(tái)機(jī)器上發(fā)送群消息。這回程序在00490890處斷下,然后跟接收緩存:

004908AC  |.  50            push    eax                              ; |Buffer = 0011F4CC
004908AD  |.  51            push    ecx                              ; |Socket
004908AE  |.  E8 C7F30C00   call    <jmp.&WSOCK32.#17>               ; \recvfrom

接收緩存Buffer的值為0011F4CC,如前所述,我們要跟進(jìn)這個(gè)地址指向的內(nèi)存的變化情況。F8單步運(yùn)行到

recvfrom后,也就是接收了網(wǎng)絡(luò)數(shù)據(jù)后,查看內(nèi)存內(nèi)容

(d 0011F4CC):

0011F4CC  31 5F 6C 62 74 34 5F 30 23 31 32 38 23 38 38 41  1_lbt4_0#128#88A
0011F4DC  45 31 44 44 34 36 36 46 44 23 30 23 30 23 37 32  E1DD466FD#0#0#72
0011F4EC  3A 31 32 39 35 37 32 31 32 31 33 3A 41 64 6D 69  :1295721213:Admi
0011F4FC  6E 69 73 74 72 61 74 6F 72 3A 50 43 2D 32 30 31  nistrator:PC-201
0011F50C  30 31 31 30 34 32 30 30 35 3A 34 31 39 34 33 33  011042005:419433
0011F51C  39 3A 5E 3B 83 A1 14 6D A4 D2 E3 D8 E8 AB B1 3A  9:^;儭mひ闔璜?
0011F52C  5B BC C2 FE E9 DA CB DD 00 BC 59 FC 9D A7 D7 91  [悸謁?糦鼭ё

內(nèi)容開(kāi)頭正是飛秋的協(xié)議頭,未加密,不過(guò)沒(méi)多大用。根據(jù)之前獲取的飛秋協(xié)議,可知,在0011F51E

處就是聊天內(nèi)容的密文。

很自然地,為了監(jiān)視這段內(nèi)存的變化情況,在該位置下內(nèi)存訪問(wèn)斷點(diǎn)(右擊數(shù)據(jù)區(qū)即可看到下斷點(diǎn)的選項(xiàng))。

F9繼續(xù)走,然后馬上斷下來(lái):

0049010F  |.  F3:A5         rep     movs dword ptr es:[edi], dword ptr [>
00490111  |.  8B4A 04       mov     ecx, dword ptr [edx+4]
00490114  |.  C74424 24 000>mov     dword ptr [esp+24], 0
0049011C  |.  894D 64       mov     dword ptr [ebp+64], ecx
0049011F  |.  33C9          xor     ecx, ecx

程序到了一個(gè)陌生的環(huán)境(在這種滿世界都是匯編代碼的情況下,幾乎一不小心就會(huì)迷失其中),看了下

附近的代碼,沒(méi)什么可疑。通過(guò)下內(nèi)存訪問(wèn)斷點(diǎn)的思路,似乎顯得荊棘叢生。

 

STEP 3 靠近目標(biāo)

不妨冷靜下來(lái)思考,如果沒(méi)有直接的路,我們可能需要悲慘地大海撈針。在寫(xiě)一個(gè)網(wǎng)絡(luò)程序時(shí),網(wǎng)絡(luò)底層

收到數(shù)據(jù)包,無(wú)非要么直接進(jìn)行邏輯處理,要么緩存到一個(gè)邏輯處理隊(duì)列里稍后處理。后者雖然對(duì)于程序員

而言是個(gè)好方法,但是因?yàn)榭缌司€程,又涉及到隊(duì)列緩存,對(duì)于逆向而言,絕對(duì)是悲劇。

 

但是如果使用了前者呢?對(duì)于一個(gè)網(wǎng)絡(luò)客戶端程序而言,也許直接進(jìn)行邏輯處理才是最KISS的方法。(這種猜測(cè)

的破解方式,絕對(duì)需要運(yùn)氣。)所以,如果是直接進(jìn)行處理,那么在接收到網(wǎng)絡(luò)數(shù)據(jù)附近,必然就有解密函數(shù)。

所以,不妨順著收包函數(shù)附近隨意瀏覽一番。(但不要走進(jìn)太深的call,不然又迷失了。)

0048FE10  /$  B8 18400000   mov     eax, 4018                          
0048FE15  |.  E8 560A0C00   call    00550870
0048FE1A  |.  8D4424 00     lea     eax, dword ptr [esp]
0048FE1E  |.  56            push    esi
0048FE1F  |.  8BF1          mov     esi, ecx
0048FE21  |.  50            push    eax
0048FE22  |.  E8 690A0000   call    00490890                             ;  接收網(wǎng)絡(luò)數(shù)據(jù)

0048FE10函數(shù)調(diào)用了剛才發(fā)現(xiàn)的收包函數(shù)。這個(gè)函數(shù)在收完數(shù)據(jù)后,不久就調(diào)用了另一個(gè)函數(shù):

0048FE3F  |.  51            push    ecx
0048FE40  |.  52            push    edx
0048FE41  |.  8BCE          mov     ecx, esi
0048FE43  |.  E8 88020000   call    004900D0                             ;  似乎很可疑?

進(jìn)到004900D0函數(shù),發(fā)現(xiàn)這個(gè)函數(shù)真TMD巨大。隨意瀏覽之,發(fā)現(xiàn)OD有這種提示:

00490178  |.  68 34FD5E00   push    005EFD34                             ;  ASCII "_lbt"
0049017D  |.  8D4C24 14     lea     ecx, dword ptr [esp+14]
00490181  |.  89BC24 544000>mov     dword ptr [esp+4054], edi

_lbt,恩,消息頭里有這個(gè)字符串標(biāo)識(shí)。估計(jì)是在做些消息頭的邏輯操作。這個(gè)函數(shù)太長(zhǎng),里面還有若干call,

可謂頭大。所以說(shuō),代碼寫(xiě)得丑,可讀性差,對(duì)于防破解還是頗有益處的。跳回到0048FE43,發(fā)現(xiàn)當(dāng)前

函數(shù)基本完了。

 

于是往上看,來(lái)到調(diào)用這個(gè)函數(shù)的地方:

0050F647  |.  E8 C407F8FF   call    0048FE10
0050F64C  |.  BF 01000000   mov     edi, 1

回顧下,我們有函數(shù)A接收網(wǎng)絡(luò)數(shù)據(jù),有函數(shù)B調(diào)用A,現(xiàn)在回到了C,來(lái)到了調(diào)用B的地方0050F647。C函數(shù)

也很巨大,直接往下瀏覽,會(huì)發(fā)現(xiàn)一些switch語(yǔ)句:

0050F71A  |.  81E6 FF000000 and     esi, 0FF
0050F720  |.  8D46 FF       lea     eax, dword ptr [esi-1]               ;  Switch (cases 1..D3)
0050F723  |.  3D D2000000   cmp     eax, 0D2
0050F728  |.  0F87 8C000000 ja      0050F7BA
0050F72E  |.  33C9          xor     ecx, ecx
0050F730  |.  8A88 60315100 mov     cl, byte ptr [eax+513160]
0050F736  |.  FF248D 403051>jmp     dword ptr [ecx*4+513040]
0050F73D  |>  8D9424 F40000>lea     edx, dword ptr [esp+F4]              ;  Case 1 of switch 0050F720

往后瀏覽下這個(gè)switch…case,發(fā)現(xiàn)非常之大,這個(gè)函數(shù)也因此非常巨大。不妨猜測(cè)這個(gè)是根據(jù)不同消息做不同

邏輯處理的地方。這個(gè)想法正是給予我們靈感的關(guān)鍵。

 

群聊消息必然也有個(gè)類(lèi)型,通過(guò)之前OD獲取到的網(wǎng)絡(luò)數(shù)據(jù)(或者截取網(wǎng)絡(luò)封包所得),群聊消息的類(lèi)型為:4194339

(16進(jìn)制400023H),去掉高位,也就是23H。仔細(xì)地對(duì)比每一個(gè)case,果然發(fā)現(xiàn)了一段處理代碼:

00512787  |> \39AC24 640100>cmp     dword ptr [esp+164], ebp             ;  Case 23 of switch 0050F720
0051278E  |.  75 07         jnz     short 00512797                       ;  群聊天處理
00512790  |.  8BC7          mov     eax, edi
00512792  |.  E9 8C080000   jmp     00513023

這個(gè)代碼之下的處理也有不少代碼。在不涉及到更多細(xì)節(jié)之前,我們可以大膽地將注意力放在接下來(lái)的call上。從這個(gè)case

往下,第一個(gè)call為:

005127E6  |.  50            push    eax
005127E7  |.  E8 A4A20000   call    0051CA90                             ;  非常可疑
005127EC  |.  B8 01000000   mov     eax, 1
005127F1  |.  E9 2D080000   jmp     00513023

 

STEP 4 多嘗試

有懷疑,就用事實(shí)來(lái)證明。果斷地在005127E6處下斷點(diǎn)。然后發(fā)群消息,程序斷下來(lái)。因?yàn)檫@個(gè)函數(shù)壓入了

eax作為參數(shù),且對(duì)ecx做了賦值:

005127E4  |.  8BCB          mov     ecx, ebx
005127E6  |.  50            push    eax
005127E7  |.  E8 A4A20000   call    0051CA90                             ;  非常可疑

在調(diào)用一個(gè)函數(shù)前對(duì)ecx做賦值,一般都是C++成員函數(shù)調(diào)用。eax作為一個(gè)參數(shù),非常值得關(guān)注,果斷查看eax

指向的內(nèi)存值:

001235C8  41 64 6D 69 6E 69 73 74 72 61 74 6F 72 00 6D 00  Administrator.m.
001235D8  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
001235E8  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
001235F8  00 00 50 43 2D 32 30 31 30 31 31 30 34 32 30 30  ..PC-20101104200
00123608  35 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  5...............
00123618  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00123628  8A 7B 00 00 C0 A8 00 03 09 79 00 00 01 00 00 00  妠..括..y.....
00123638  04 00 00 00 00 00 00 00 80 00 00 00 38 38 41 45  .......€...88AE
00123648  31 44 44 34 36 36 46 44 00 00 00 00 00 00 00 00  1DD466FD........
00123658  00 00 00 00 F4 C7 23 00 FD 22 3B 4D 23 00 40 00  ....羥#.?;M#.@.
00123668  49 00 00 00 36 00 00 00 5E 3B 83 A1 14 6D A4 D2  I...6...^;儭mひ

有用戶名、機(jī)器名、發(fā)送者M(jìn)AC地址,這么多可疑信息。完全可以猜測(cè),eax傳入的是一個(gè)結(jié)構(gòu)體地址,

當(dāng)然對(duì)象地址也可以,反正是個(gè)復(fù)雜數(shù)據(jù)結(jié)構(gòu)。更重要的,在這塊內(nèi)存往下不遠(yuǎn)處,果斷地發(fā)現(xiàn)了從

網(wǎng)絡(luò)接收到的加密的聊天內(nèi)容:

00123670  5E 3B 83 A1 14 6D A4 D2 E3 D8 E8 AB B1 3A 5B BC  ^;儭mひ闔璜?[
00123680  C2 FE E9 DA CB DD 00 BC 59 FC 9D A7 D7 91 CF 5A  漫櫞溯.糦鼭ё懴Z

F8直接步過(guò)0051CA90函數(shù)。任務(wù)欄開(kāi)始出現(xiàn)閃爍的圖標(biāo)(雖然沒(méi)有閃),上面的內(nèi)存數(shù)據(jù)變了:

00123670  74 65 73 74 7B 2F 66 6F 6E 74 3B 2D 31 34 20 30  test{/font;-14 0
00123680  20 30 20 30 20 34 30 30 20 30 20 30 20 30 20 31   0 0 400 0 0 0 1
00123690  33 34 20 33 20 32 20 31 20 32 20 CB CE CC E5 20  34 3 2 1 2 宋體

test正是我發(fā)的內(nèi)容。

 

STEP 5 縮小范圍

實(shí)際上走到這里,憑借運(yùn)氣和程序編寫(xiě)的常識(shí),已經(jīng)找到關(guān)鍵點(diǎn)。不過(guò)看來(lái)0051CA90這個(gè)函數(shù)做的事情

有點(diǎn)多,除了解密內(nèi)容似乎還有UI上的一些處理(例如那個(gè)閃爍的任務(wù)欄圖標(biāo))。所以,我們要做的是,進(jìn)一步

跟進(jìn),找到關(guān)鍵點(diǎn)。

 

現(xiàn)在縮小范圍要容易得多,因?yàn)槲覀兊玫搅艘粋€(gè)會(huì)變化的內(nèi)存地址:00123670。只需要F8一步一步地

運(yùn)行代碼,一旦發(fā)現(xiàn)內(nèi)存內(nèi)容改變,則可以進(jìn)一步進(jìn)如,從而找到關(guān)鍵call。具體過(guò)程我就不給了,大概為:

0051CB02  |.  52            push    edx
0051CB03  |.  68 00400000   push    4000
0051CB08  |.  56            push    esi
0051CB09  |.  50            push    eax
0051CB0A  |.  56            push    esi
0051CB0B  |.  E8 F041F7FF   call    00490D00                             ; 跟進(jìn)

 

00490DB0  |.  6A 00         push    0
00490DB2  |.  83E1 03       and     ecx, 3
00490DB5  |.  6A 22         push    22
00490DB7  |.  F3:A4         rep     movs byte ptr es:[edi], byte ptr [es>
00490DB9  |.  8BBC24 344000>mov     edi, dword ptr [esp+4034]
00490DC0  |.  50            push    eax                                  ;  數(shù)據(jù)長(zhǎng)度
00490DC1  |.  8D4424 20     lea     eax, dword ptr [esp+20]
00490DC5  |.  57            push    edi                                  ;  輸出緩存
00490DC6  |.  50            push    eax                                  ;  輸入緩存(加密內(nèi)容)
00490DC7  |.  8D4C24 20     lea     ecx, dword ptr [esp+20]
00490DCB  |.  E8 5049F7FF   call    00405720                             ;  最終解密函數(shù)

 

00405720函數(shù)內(nèi)的實(shí)現(xiàn)基本上全是數(shù)據(jù)操作指令。加解密算法無(wú)非就是搗鼓這些數(shù)據(jù),所以當(dāng)我進(jìn)到

00405720函數(shù)時(shí),基本上可以斷定它就是最終的解密函數(shù)。

 

STEP 6 解密

事實(shí)上我們并不需要去弄懂它的具體解密算法,就算是直接的C++代碼,沒(méi)有算法論文的話也很難看懂,更何況

是這里的匯編。最直接的方式,就是查看這個(gè)解密函數(shù)對(duì)外界的依賴情況,例如需要的參數(shù),this里是否有依賴

的數(shù)據(jù)。在了解了這些情況后,大可以將這段匯編復(fù)制出來(lái)直接作為C++內(nèi)嵌匯編代碼使用。

 

不過(guò),這里我想到了更簡(jiǎn)單的方式。因?yàn)槲易⒁獾斤w秋和飛鴿在實(shí)現(xiàn)上有著不解之緣,而且我琢磨著作者也不會(huì)

為了一個(gè)加解密不太重要的應(yīng)用場(chǎng)合而去整些高深的算法。我想到,飛秋也許會(huì)直接使用飛鴿里的加解密代碼!

 

在IP message的源碼里,有blowfish加密算法的實(shí)現(xiàn),我們來(lái)看接口:

class CBlowFish
{
private:
    DWORD    *PArray;
    DWORD    (*SBoxes)[256];
    void    Blowfish_encipher(DWORD *xl, DWORD *xr);
    void    Blowfish_decipher(DWORD *xl, DWORD *xr);

public:
            CBlowFish(const BYTE *key=NULL, int keybytes=0);
            ~CBlowFish();
    void    Initialize(const BYTE *key, int keybytes);
    DWORD    GetOutputLength(DWORD lInputLong, int mode);
    DWORD    Encrypt(const BYTE *pInput, BYTE *pOutput, DWORD lSize, int mode=BF_CBC|BF_PKCS5, _int64 IV=0);
    DWORD    Decrypt(const BYTE *pInput, BYTE *pOutput, DWORD lSize, int mode=BF_CBC|BF_PKCS5, _int64 IV=0);
};

從接口實(shí)現(xiàn)來(lái)說(shuō)算是簡(jiǎn)潔漂亮友好和諧。我也用Decrypt這個(gè)函數(shù)的參數(shù)比對(duì)了上面沒(méi)找到的那個(gè)call(00405720),

因?yàn)檫@里只是懷疑這個(gè)call就是這里的Decrypt,但并沒(méi)有確切的證據(jù)。不過(guò),對(duì)比下他們的參數(shù)就可以非常肯定了:

00490DB0  |.  6A 00         push    0           ;參數(shù)IV
00490DB2  |.  83E1 03       and     ecx, 3
00490DB5  |.  6A 22         push    22        ;參數(shù)mode
00490DB7  |.  F3:A4         rep     movs byte ptr es:[edi], byte ptr [es>
00490DB9  |.  8BBC24 344000>mov     edi, dword ptr [esp+4034]
00490DC0  |.  50            push    eax                                  ; 參數(shù) 數(shù)據(jù)長(zhǎng)度
00490DC1  |.  8D4424 20     lea     eax, dword ptr [esp+20]
00490DC5  |.  57            push    edi                                  ;  參數(shù)輸出緩存
00490DC6  |.  50            push    eax                                  ; 參數(shù)輸入緩存(加密內(nèi)容)
00490DC7  |.  8D4C24 20     lea     ecx, dword ptr [esp+20]
00490DCB  |.  E8 5049F7FF   call    00405720                             ;  最終解密函數(shù)

最重要的,是參數(shù)mode。Decrypt默認(rèn)參數(shù)mode是BF_CBC|BF_PKCS5的位組合,結(jié)果,恰好為22!

所以,基本上可以斷定:飛秋的加解密實(shí)現(xiàn),就是使用了IP message的blowfish算法:blowfish.cpp/h/h2。

 

STEP 7 密鑰

查看CBlowFish的使用,在解密前需要初始化,大概就是傳入密鑰之類(lèi)。如果我們上面的猜測(cè)沒(méi)有錯(cuò),那么我們

從網(wǎng)絡(luò)上取得的數(shù)據(jù),然后取得密鑰,直接使用blowfish的源碼,就可以解密出消息內(nèi)容。

 

接下來(lái)的關(guān)鍵就是,找到這個(gè)密鑰。關(guān)于這個(gè)密鑰,之前在飛秋的配置文件FeiqCfg.xml里繞了很久的圈子,因?yàn)?/p>

發(fā)現(xiàn)加入一個(gè)群的時(shí)候,這個(gè)文件里就會(huì)多出一項(xiàng)很長(zhǎng)的16進(jìn)制字符串。也一度猜測(cè)密鑰就是保存在這個(gè)字符串的

某個(gè)偏移里。接下來(lái)會(huì)讓人大跌眼鏡。

 

因?yàn)镃BlowFish這個(gè)類(lèi)確實(shí)簡(jiǎn)單,使用它的最簡(jiǎn)單方式就是直接創(chuàng)建局部對(duì)象,然后傳入key和keysize,即可完成

初始化。在之前展示的思路里,我也一度先去嘗試最簡(jiǎn)單的方法。對(duì)于C++局部對(duì)象的創(chuàng)建,有個(gè)顯著特征就是

mov ecx, dword ptr [esp+xxx],也就是往ecx里寫(xiě)入一個(gè)棧地址。而且可以肯定的是,這個(gè)構(gòu)造代碼,必然發(fā)生

于call 00405720前面不遠(yuǎn)處,往上跟進(jìn):

00490D3F  |> \8B8C24 304000>mov     ecx, dword ptr [esp+4030]
00490D46  |>  51            push    ecx                                  
00490D47  |.  52            push    edx                                  
00490D48  |.  8D4C24 0C     lea     ecx, dword ptr [esp+C]
00490D4C  |.  E8 3F3DF7FF   call    00404A90                             

一個(gè)壓入兩個(gè)參數(shù)的函數(shù)調(diào)用,對(duì)比CBlowFish的構(gòu)造函數(shù),剛好是2個(gè)參數(shù)。跟進(jìn)00404A90:

 

00404A90  /$  56            push    esi
00404A91  |.  8BF1          mov     esi, ecx
00404A93  |.  6A 48         push    48
00404A95  |.  E8 69301600   call    00567B03
00404A9A  |.  68 00100000   push    1000
00404A9F  |.  8906          mov     dword ptr [esi], eax
00404AA1  |.  E8 5D301600   call    00567B03

 

又是可愛(ài)的立即數(shù)!48H、1000H,這種特別的立即數(shù)總能讓人安心,對(duì)比CBlowFish構(gòu)造函數(shù)實(shí)現(xiàn):

CBlowFish::CBlowFish (const BYTE *key, int keybytes)
{
    PArray = new DWORD [NPASS + 2];//NPASS=16
    SBoxes = new DWORD [4][256];

    if (key)
        Initialize(key, keybytes);
}

sizeof(DWORD)*18=48H,sizeof(DWORD)*4*256=1000H!這么極具喜劇意義的匯編C++代碼映射,

基本可以肯定,00404AA1處,正是構(gòu)造CBlowFish對(duì)象的地方,而構(gòu)造的參數(shù),正是我們魂?duì)繅?mèng)縈的解密密鑰:

 

00490D46  |> \51            push    ecx                                  ;  key長(zhǎng)度
00490D47  |.  52            push    edx                                  ;  密鑰key
00490D48  |.  8D4C24 0C     lea     ecx, dword ptr [esp+C]
00490D4C  |.  E8 3F3DF7FF   call    00404A90                             ;  構(gòu)造blowfish對(duì)象

 

在此處下斷點(diǎn),發(fā)送群消息,程序斷下來(lái),看看密鑰究竟是什么。如果它確實(shí)是FeiqCfg.xml里的某個(gè)值,

那么我們還要進(jìn)一步跟這個(gè)值具體在哪個(gè)配置項(xiàng)里。看看吧,密鑰edx:

 

edx=00123644, (ASCII "88AE1DD466FD")

 

 

TM的密鑰居然是發(fā)送者的MAC地址!當(dāng)我看到這個(gè)的時(shí)候我?guī)缀蹩焖さ沟厣稀H绻w秋使用一個(gè)MAC地址

作為密鑰,那么這意味著:通過(guò)自己寫(xiě)的程序,可以取得局域網(wǎng)內(nèi)其他群里的聊天內(nèi)容!這個(gè)實(shí)在太邪惡了。

上回抓包的時(shí)候,雖然看不到內(nèi)容,但可以看到美術(shù)、策劃在群里聊的無(wú)比歡樂(lè)。這回有喜感了。

 

STEP END 可略

看看時(shí)間,悲劇地發(fā)現(xiàn)整篇文章花了接近3個(gè)小時(shí)才寫(xiě)完。此刻我正躊躇要不要把代碼發(fā)上來(lái),但轉(zhuǎn)念一想

最后那個(gè)STEP的發(fā)現(xiàn)實(shí)在讓人蛋疼,所以還是算了。打算稍微封裝下,然后使用這份代碼在iptux 上改改包裝

個(gè)界面,目的就算達(dá)成了。相信瀏覽完整篇文章,寫(xiě)出自己的代碼不是什么大問(wèn)題。

 

其實(shí)我大可以直接給結(jié)論,但是我依然樂(lè)于分享過(guò)程和思路。一來(lái)算是自我總結(jié)記錄(每次拿起OD,總是快捷

鍵一路忘);二來(lái)也給有心人一個(gè)指引。

 

最后,對(duì)這種東西還是有必要出個(gè)免責(zé)聲明:根據(jù)本文章所造成的一切后果與文章作者無(wú)關(guān)。為了不糟蹋我這3個(gè)

小時(shí)的時(shí)間,轉(zhuǎn)載麻煩注明出處。

PS,最后回顧下結(jié)論,其實(shí)發(fā)現(xiàn)這個(gè)解密非常非常簡(jiǎn)單。你說(shuō)如果直接給盧本陶(飛秋作者)發(fā)封郵件,他會(huì)不

會(huì)直接告訴我?

posted @ 2011-01-23 21:01 Kevin Lynx 閱讀(25906) | 評(píng)論 (9)編輯 收藏

一段tricky codes:函數(shù)調(diào)用的那些底層細(xì)節(jié)


有一天,被同事問(wèn)到了下面這段代碼,就簡(jiǎn)單分析了一下,發(fā)覺(jué)還有點(diǎn)意思:

__declspec(naked)
void call(void* pfn, 
{
    __asm 
    
{
        pop eax;
        add eax, 
3;
        xchg dword ptr[esp], eax;
        push eax;
        ret;
    }

}

 

再看它的用法:

 

void print_str( const char *s )
{
    printf( 
"%s\n", s );
}

call( print_str, 
"a string" );

 

call函數(shù)的大致作用,就是調(diào)用傳遞進(jìn)去的函數(shù)print_str,并將參數(shù)"a string"傳遞給目標(biāo)
函數(shù)。

但是它是怎么做到的呢?雖然call只有簡(jiǎn)單的幾句匯編代碼,但是卻包含了很多函數(shù)在編譯
器中的匯編層實(shí)現(xiàn)。要了解這段代碼的意思,需要知道如下相關(guān)知識(shí):

0、函數(shù)調(diào)用的實(shí)現(xiàn)中,編譯器通過(guò)系統(tǒng)堆棧(ESP寄存器指向)傳遞參數(shù);
1、C語(yǔ)言默認(rèn)的函數(shù)調(diào)用規(guī)則(_cdecl)中,調(diào)用者從右往左將參數(shù)壓入堆棧,并且調(diào)用者負(fù)
責(zé)堆棧平衡,也就是保證調(diào)用函數(shù)的前后,ESP不變;
2、匯編指令call本質(zhì)上是先將返回地址,通常是該條指令的下一條指令壓入堆棧,然后直
接跳轉(zhuǎn)到目標(biāo)位置;
3、匯編指令ret則是先從堆棧棧頂取出返回地址,然后跳轉(zhuǎn)過(guò)去;
4、匯編指令add加上其操作數(shù),貌似占3個(gè)字節(jié)長(zhǎng)度;
5、在visual studio中,DEBUG模式下編譯器會(huì)在我們的代碼中插入各種檢測(cè)代碼,而
__declspec(naked)則是告訴編譯器:別往這里添加代碼。

了解了以上常識(shí)后,再看這段代碼,其本質(zhì)無(wú)非就是利用了這些規(guī)則,在代碼段跳來(lái)跳去。
我們來(lái)逐步分析一下:

在調(diào)用call函數(shù)的地方,大概的代碼為:

 

caller:
// 堆棧狀態(tài),從左往右分別表示棧頂至下
// ret_addr是call后的地址,即add esp, 8的位置
// a1, a2表示函數(shù)參數(shù),callee_addr是這里的print_str
// stack: ret_addr, callee_addr, a1, a2, 
call( print_str, "a string" ); 
add esp, 
8 //清除參數(shù)傳遞所占用的堆棧空間,維持堆棧平衡
end_label //位于add后的指令,后面會(huì)提到

call:
// 此時(shí)堆棧stack: ret_addr, a1, a2
pop eax // eax = ret_addr; stack: callee_addr, a1, a2, 
add eax, 3 // eax = end_label; stack: callee_addr, a1, a2, 
xchg dword ptr[esp], eax // eax = callee_addr; stack: end_label, a1, a2, 
push eax // stack: callee_addr, end_label, a1, a2, 
ret // 取出callee_addr并跳轉(zhuǎn),也就跳轉(zhuǎn)到print_str函數(shù)的入口,此時(shí)堆棧
    
// stack: end_label, a1, a2, 

callee(print_str):

 無(wú)視函數(shù)內(nèi)容

ret 
// print_str返回,此時(shí)正常情況下,堆棧stack: end_label, a1, a2, 
 
// 取出end_label并跳轉(zhuǎn),stack: a1, a2, 

 

那么當(dāng)callee結(jié)束時(shí),則跳轉(zhuǎn)回caller函數(shù)中。不過(guò),如過(guò)你所見(jiàn),此時(shí)堆棧中還保留著再
調(diào)用call函數(shù)時(shí)傳入的參數(shù):stack: a1, a2, ...,所以,DEBUG模式下,VS就會(huì)提示你堆
棧不平衡。這里簡(jiǎn)單的處理就是手動(dòng)來(lái)進(jìn)行堆棧平衡:

 

    call( print_str, "a string" );
    __asm
    
{
        add esp, 
4
    }

 

傳入了多少個(gè)參數(shù),就得相應(yīng)地改變esp的值。

話說(shuō)距離上篇博客都有半年了,自己都不知道時(shí)間晃得如此之快。最近業(yè)余折騰了下android開(kāi)發(fā)
一不小心就跨年了。
 

posted @ 2011-01-02 16:34 Kevin Lynx 閱讀(4932) | 評(píng)論 (4)編輯 收藏

網(wǎng)游中的玩家移動(dòng)

     摘要: MMORPG中,玩家的移動(dòng)主要邏輯都放在客戶端進(jìn)行,包括自動(dòng)尋路和響應(yīng)玩家的操作,服務(wù)
器在這里擔(dān)當(dāng)一個(gè)被動(dòng)角色。但是服務(wù)器端的玩家數(shù)據(jù)卻是真正的被其他邏輯模塊參考的數(shù)
據(jù)。  閱讀全文

posted @ 2010-06-22 21:27 Kevin Lynx 閱讀(4781) | 評(píng)論 (8)編輯 收藏

僅列出標(biāo)題
共12頁(yè): First 2 3 4 5 6 7 8 9 10 Last 
青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            国产真实久久| 久久亚洲综合色一区二区三区| 一本色道久久综合亚洲二区三区| 激情欧美一区二区三区| 亚洲成人在线免费| 亚洲精品一区二区在线| 夜夜嗨av一区二区三区免费区| 一区二区精品| 久久精品九九| 亚洲高清不卡| 一二三四社区欧美黄| 性久久久久久久| 久久精品欧美日韩精品| 免费看av成人| 亚洲激情一区二区| 在线性视频日韩欧美| 欧美在线综合| 欧美日韩在线一区二区| 国产人妖伪娘一区91| 亚洲国产综合91精品麻豆| 亚洲一区二区三区在线观看视频 | 亚洲国产mv| 亚洲精品中文字| 久久激情中文| 欧美日韩另类丝袜其他| 国产字幕视频一区二区| 亚洲午夜视频| 欧美大片在线观看一区| 亚洲一区欧美一区| 欧美黑人一区二区三区| 国内精品伊人久久久久av一坑| 日韩午夜av电影| 噜噜爱69成人精品| 亚洲欧美精品伊人久久| 欧美日韩喷水| 亚洲精品乱码久久久久久蜜桃麻豆 | 狠狠做深爱婷婷久久综合一区 | 日韩午夜一区| 久久综合国产精品台湾中文娱乐网| 国产精品国产三级国产普通话蜜臀| 亚洲国产综合在线| 麻豆精品视频在线观看视频| 亚洲嫩草精品久久| 欧美午夜一区二区福利视频| 亚洲狼人综合| 亚洲二区视频| 米奇777超碰欧美日韩亚洲| 极品少妇一区二区三区精品视频| 亚洲欧美日韩国产中文| 日韩亚洲欧美中文三级| 欧美人成在线视频| 一本色道久久精品| 亚洲精品乱码久久久久久蜜桃麻豆| 久久永久免费| 亚洲国产老妈| 欧美激情一区二区三区在线视频观看| 欧美一级视频| 国产一区二区0| 久久久人成影片一区二区三区| 亚洲激情电影在线| 国产农村妇女精品一二区| 一区二区三区视频在线播放| 亚洲高清视频在线| 欧美jjzz| 一区二区91| 99re6热在线精品视频播放速度| 欧美久久久久久久久| 一本色道久久99精品综合| 99re热精品| 国产精品视频xxxx| 久久久精彩视频| 久久噜噜亚洲综合| 亚洲精品美女在线观看播放| 91久久国产精品91久久性色| 欧美日韩国产精品一区二区亚洲| 中文在线不卡视频| 亚洲免费视频一区二区| 国产亚洲女人久久久久毛片| 久久综合久久综合这里只有精品| 久久视频国产精品免费视频在线| 亚洲韩国精品一区| 妖精成人www高清在线观看| 国产精品久久久久毛片软件 | 亚洲黄色成人| 国产精品欧美经典| 免费人成精品欧美精品| 欧美精品videossex性护士| 亚洲一区二区久久| 久久久噜久噜久久综合| 夜夜嗨av一区二区三区中文字幕 | 欧美成人精品激情在线观看| 麻豆免费精品视频| 伊人激情综合| 亚洲国产91| 国产精品美女久久久浪潮软件 | 亚洲欧美在线网| 亚洲高清视频中文字幕| 9l视频自拍蝌蚪9l视频成人| 国产综合色在线| 亚洲区中文字幕| 国产在线精品成人一区二区三区 | 亚洲激情在线视频| 国产精品日本| 亚洲第一精品久久忘忧草社区| 国产精品久久久久999| 欧美国产日韩亚洲一区| 国产精品亚洲第一区在线暖暖韩国| 欧美刺激性大交免费视频| 国产精品免费区二区三区观看| 欧美黄色aaaa| 狠狠综合久久av一区二区小说 | 欧美日韩在线播| 欧美专区在线播放| 欧美日韩国产欧| 欧美成人69| 激情综合在线| 欧美在线播放| 欧美在线不卡视频| 国产精品成人av性教育| 亚洲精品国产精品国自产观看| 激情综合亚洲| 久久精品国产亚洲a| 欧美亚洲一级片| 欧美色一级片| 亚洲靠逼com| 一区二区日韩免费看| 欧美福利一区二区| 欧美激情精品久久久久久久变态 | 久久精品国产99精品国产亚洲性色| 久久综合久久综合久久综合| 久久se精品一区二区| 国产精品久久77777| 99爱精品视频| 亚洲婷婷综合色高清在线| 欧美日本中文字幕| 亚洲欧洲综合| 99re6这里只有精品| 欧美黑人国产人伦爽爽爽| 最新69国产成人精品视频免费| 91久久亚洲| 欧美激情综合五月色丁香| 亚洲人体1000| 亚洲一区欧美| 国产精品一区在线观看| 午夜亚洲视频| 麻豆精品网站| 亚洲精品四区| 欧美日韩一区二区视频在线 | 今天的高清视频免费播放成人 | 国产精品私房写真福利视频| 亚洲女性喷水在线观看一区| 欧美一区二区三区在线观看视频| 国产精品专区第二| 久久久精品动漫| 国产精品网站一区| 亚洲靠逼com| 欧美在线日韩在线| 久久婷婷久久| 亚洲日本中文字幕区| 欧美日韩免费观看一区三区| 亚洲一区二区视频在线观看| 久久久久国色av免费看影院| 91久久国产自产拍夜夜嗨 | 国产精品亚洲第一区在线暖暖韩国| 久久精品亚洲精品| 日韩午夜激情| 亚洲资源在线观看| 韩国一区二区三区美女美女秀| 亚洲老司机av| 欧美日韩免费观看一区=区三区| 亚洲自拍偷拍福利| 欧美成人a视频| 亚洲免费在线播放| 亚洲国产视频直播| 国产精品一区二区久久精品| 久久久久久久久久久久久女国产乱| 欧美激情一区三区| 久久久久久网站| 亚洲永久免费精品| 亚洲国产成人tv| 国产麻豆精品视频| 欧美精品一区二区三区在线播放| 欧美一区二区三区成人| 亚洲精品一区在线观看| 久久综合一区| 欧美在线视频观看免费网站| 亚洲精品一区在线观看| 国产亚洲精品高潮| 欧美午夜一区二区三区免费大片| 久久这里只精品最新地址| 午夜精品www| 亚洲一区免费| 99精品久久久| 亚洲精品日本| 欧美国内亚洲| 欧美插天视频在线播放| 好吊视频一区二区三区四区| 国产精品国产福利国产秒拍| 欧美国产日韩免费| 免费观看在线综合色|