• <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>

            loop_in_codes

            低調做技術__歡迎移步我的獨立博客 codemaro.com 微博 kevinlynx

            #

            博客遷移

            博客遷移

            3年前我從CSDN博客遷移到CPPBLOG寫博客。然后我亂七八糟地寫一篇博客聲明。那個時候還 沒把寫博客當一回事,純碎是出于為自己做記錄的目的。如今寫博客一半還是出于記錄,另 一半則是出于在外面自我表現,還有那么一點點說出來覺得崇高的理由就是分享自己的知識 。

            這回我倒不是遷移出CPPBLOG。CPPBLOG整體上是個更低調做技術的圈子,一如我博客的副標 題。自己建了個獨立博客,主要還是因為學習Lisp的原因。

            在過去的若干年中,有各種各樣的技術點因為長久的疏遠,一個一個淡出于大腦。雖說技術 點不大重要,學習能力、方法才是終生受用的東西。但完全忘記,也算是一大遺憾。Lisp不 應該被我忘記,一個程序員至少應該在他常用的語言之外,熟悉另一門語言。而這另一門語 言應該是更“快速的“、更“方便的“,它能在你需要的時候更快速地構建你的想法。雖然出于 工作原因,我得時不時地搗鼓下Lua。

            Anyway,這個獨立博客就是我用Lisp搭建的。從Web server到博客系統。其中博客系統也有 我很大一部分代碼在其中,在可預見的未來,在Lisp應用方面我將主要用于構建這個博客系 統。靠自己寫的代碼(雖然只有一部分)搭建出來的博客就是爽啊。如果要我用個現成的, 我還真懶得去獨立門戶。

            程序員嘛,只有在不斷地自我折騰(簡寫自虐)中,才能成長。最后,請求大家(+強迫) 移步我的新博客 codemacro.com 。要參考技術信息的,可以 參考這里 ,要看看我使用 Lisp是如何搗鼓出這個博客系統的,可以 點這里 。當然,在可預見的未來,我還是會在 CPPBLOG同步更新的。 。

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

            傳遞Lua函數到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 閱讀(11529) | 評論 (9)編輯 收藏

            淺談代碼分層:構建模塊化程序

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

            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 閱讀(18973) | 評論 (5)編輯 收藏

            Lisp實踐:開發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://www.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://www.shnenglu.com/rss.aspx
            

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

            GET /rss.aspx HTTP/1.0
            Host: www.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 閱讀(4076) | 評論 (0)編輯 收藏

            Lisp一瞥:增強型變量Symbol

            Lisp一瞥:增強型變量Symbol

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

            Note

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

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

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

            Lisp中的“變量"

            很多教授Lisp的書中,大概會簡單地告訴我們可以使用如下的方式定義一個全局變量 [1].

            (defparameter *var* 1)
            

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

            (defvar *var* 1)
            

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

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

            好了,就這些了。Lisp中關于變量的細節,也就這些。你甚至能用你在C/C++中的經驗來窺 探一切。但是,我們很快就看到了很多困惑的地方。

            我遇到的第一個困惑的地方來源于函數,那么等我講講函數再來分享下坎坷。

            Lisp中的函數

            Lisp中的函數絕對不復雜,你絕對不用擔心我在忽悠你 [3] 。作為一門函數式語言,其首要 任務就是加強函數這個東西在整個語言里的功能。如果你喜歡廣閱各種與你工作不相干的 技術,你肯定已經對很多函數式語言世界中的概念略有耳聞。例如閉包,以及first class type [4]

            Lisp中的函數就是first class type。這什么意思呢?直白來說, Lisp中的函數和變量 沒什么區別,享有同等待遇 。進一步來說,變量fn的值可以是數值1,也可以是字符串 "hello",甚至是某個函數。這其實就是C++程序員說的functor。

            Lisp中定義函數非常簡單:

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

            這樣,我們就定義了一個名為add2,有1個參數,1個返回值的函數。要調用該函數時,只需 要 (add2 2) 即可。這簡直和我們在Lisp中完成一個加法一模一樣:(+ 2 3)

            Lisp作為一門函數式語言,其函數也能作為另一個函數的參數和返回值 [5]

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

            apply-fn函數第一個參數是一個函數,它使用funcall函數間接地調用fn指向的函數。作為 一個C++程序員,這簡直太好理解了,這完全就是一個函數指針的語法糖嘛。于是,假設我 們要使用apply-fn來間接調用add2函數:

            (apply-fn add2 2) ;; wrong
            

            可是這是不對的。我們需要通過另一個特殊操作符來完成這件事:

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

            #'操作符用于將add2對應的函數取出來,這么說當然不大準確。Again,作為一個C++程序員 ,這簡直就是個取地址操作符&的語法糖嘛。好吧,這么理解起來似乎沒問題了。

            Lisp中能甚至能在任何地方定義一個函數,例如我們創建一個函數,該函數返回創建出來的 函數,這是一個典型的講解什么是 閉包 的例子:

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

            無論如何,get-add-n函數返回一個函數,該函數是add2函數的泛型實現。它可以將你傳入 的參數加上n。這些代碼里使用了lambda表達式。lambda表達式直白來說,就是創建一個字 面上的函數。這又是什么意思呢?就像我們在代碼中寫出2,寫出"hello"一樣,2就是個字 面上的數字,"hello"就是個字面上的字符串 [6]

            那么,總而言之,通過lambda創建一個函數體,然后通過#'操作符即可得到一個函數,雖然 沒有名字。有了以上知識后,Again and again,作為一個C++程序員,很快我們就能得到一 個程序:定義變量,用變量去保存一個函數,然后通過這個變量來調用這個函數。這是多么 天經地義的事,就像之前那個通過參數調用其指向的函數一樣:

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

            這樣的代碼是不對的,錯誤發生于第二行,無論你使用的Lisp實現是哪種,大概會得到如下 的錯誤信息:

            "The function FN is undefined."
            

            老實說,這已經算是多么有跡可循的錯誤提示了啊。將以上代碼和之前的apply-fn對比,是 多么得神似啊,可惜就是錯的。這是我們遇到的第一個理解偏差導致的問題。如果你還不深 入探究,你將會在這一塊遇到更多麻煩。及時地拿出你的勇氣,披荊斬棘,刨根究底,絕對 是學習編程的好品質。

            “萬惡之源“:SYMBOL

            上文中提到的變量函數之類,之所以會在某些時候與我們的理解發生偏差,并且總是存在些 神秘的地方無法解釋。這完全是因為我們理解得太片面導致。Lisp中的Symbol可以說就是某 個變量,或者某個函數,但這太片面。Lisp中的Symbol擁有更豐富的含義。

            Symbol的名字

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

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

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

            Symbol本質

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

            imgs/symbol-obj.png

            通過這幅圖,可以揭開所有謎底。一個Symbol包含至少圖中的幾個域,例如Name、Value、 Function等。在Lisp中有很多函數來訪問這些域,例如上文中使用到的symbol-name,這個 函數本質上就是取出一個Symbol的Name域。

            Symbol與Variable和Function的聯系

            自然而然地,翻閱Lisp文檔,我們會發現果然還有其他函數來訪問Symbol的其他域,例如:

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

            但是這些又與上文提到的變量和函數有什么聯系呢?真相只有一個, 變量、函數粗略來 說就是Symbol的一個域,一個成員。變量對應Value域,函數對應Function域。一個Symbol 這些域有數據了,我們說它們發生了綁定(bind)。 而恰好,我們有幾個函數可以用于判 定這些域是否被綁定了值:

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

            通過一些代碼來回味以上結論:

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

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

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

            More More More

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

            當一個Symbol出現在一個List的第一個元素時,它被處理為函數。這么說有點迷惑人,因為 它帶進了Lisp中代碼和數據之間的模糊邊界特性。簡單來說,就是當Symbol出現在一個括號 表達式(s-expression)中第一個位置時,算是個函數,例如:

            (add2 3) ; add2位于第一個位置,被當作函數處理
            (*var* 3) ; 這里*var*被當作函數調用,返回3
            

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

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

            這看起來是多么古怪的代碼。但是運用我們上面說的結論,便可輕易解釋:表達式中第一個 *var* 被當作函數處理,它需要一個參數;表達式第二部分的 *var* 被當作變量 處理,它的值為1,然后將其作為參數傳入。

            再來說說'(quote)操作符,這個操作符用于防止其操作數被求值。而當一個Symbol出現時, 它總是會被求值,所以,我們可以分析以下代碼:

            (symbol-value *var*) ; wrong
            

            這個代碼并不正確,因為 *var* 總是會被求值,就像 (*var* *var*) 一樣,第二 個 *var* 被求值,得到數字1。這里也會發生這種事情,那么最終就等同于:

            (symbol-value 1) ; wrong
            

            我們試圖去取數字1的Value域,而數字1并不是一個Symbol。所以,我們需要quote運算符:

            (symbol-value '*var*) ; right
            

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

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

            現在,我們甚至能解釋上文留下的一個問題:

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

            給fn的Value賦值一個函數, (fn 3) 當一個Symbol作為函數使用時,也就是取其 Function域來做調用。但其Function域什么也沒有,我們試圖將一個Symbol的Value域當作 Function來使用。如何解決這個問題?想想,symbol-function可以取到一個Symbol的 Function域:

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

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

            (apply-fn add2 2) ; wrong
            

            本意是想傳入add2這個Symbol的function域,但是直接這樣寫的話,傳入的其實是add2的 Value域 [7] ,這當然是不正確的。對比正確的寫法,我們甚至能猜測#'運算符就是一個 取Symbol的Function域的運算符。進一步,我們還可以給出另一種寫法:

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

            深入理解事情的背后,你會發現你能寫出多么靈活的代碼。

            END

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

            注解

            [1] 在Lisp中全局變量又被稱為dynamic variables
            [2] Lisp中按照習慣通常在為全局變量命名時會加上星號,就像我們習慣使用g_一樣
            [3] 因為我確實在忽悠你
            [4] first class type,有人翻譯為“一等公民”,我覺得壓力巨大
            [5] 即高階函數
            [6] “字面“主要是針對這些信息會被詞法分析程序直接處理
            [7] 這可能導致更多的錯誤

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

            用lisp開發博客客戶端

            用lisp開發博客客戶端

            Author: Kevin Lynx
            Date: 3.13.2011

            最近一直在學習Lisp這門語言。回頭一看,基本上接近1個月了。剛開始接觸Lisp是因為看 了<Lisp本質>,然后我發現有很多人宗教般地忠誠這門語言,于是就來了興趣。

            imgs/lisp_believer.png

            當然并不是每次因為某篇寫得很geek技術文章就去學習某個新的技術點。一個月時間對我來 說還是很珍貴了。但是Lisp絕對是大部分程序員都值得一學的語言(就像Haskell一樣)。 我能給出的簡單理由包括:

            • 大部分程序員只會命令式語言(C/C++/C Like etc),缺乏函數式語言解決編程問題的思 想(當然Lisp不是純函數式)
            • Lisp是僅次于Fortran的古老語言,很多優秀的語言設計思想在現代的一些語言里都找得 到
            • 裝B黨必備

            另一方面,結合我一個月以來的讀書和兩個練習工程的實踐經歷,我覺得也有些理由值得你 不去學習Lisp:

            • 你會Haskell或者其他函數式語言
            • 我目前還是覺得Lisp學習曲線高(大概是因為我讀到的書都在應用語法層兜圈子,事實上 Lisp的語法之統一,全特么的是s-expression),你不愿意花費這些成本
            • you are too old bo to be a B

            關于這篇文檔

            這篇博客我使用reStructuredText格式編寫,然后用docutls導出為html,再然后使用這回 用lisp開發的基于metaweblog API的博客客戶端,自動發布到CPPBLOG。

            他們怎么說Lisp

            我就摘錄些書上的觀點(歷史):

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

            And my opinion

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

            • Lisp也是一門雜和型風格的語言,函數式、命令式、面向對象,以及最被人吹捧的宏編程 --程序自己寫自己
            • Lisp的語句全部以(xxx xxx)的形式出現,被稱為s-expression,我看稱為括號表達式還 差不多
            • Lisp每條語句都有返回值,沒基礎過函數式編程的同學,if語句也是有返回值的
            • 函數式編程語言的一個重要特性就是閉包(closure),這個東西用來避免全局變量實在太 geek了

            開始學習Lisp

            Lisp不像有些語言,有個直接的機構來維護。感覺它更像C/C++一樣,只有個標準,然后有 若干編譯器(解釋器)實現。Lisp在幾十年的發展中,產生了很多種方言。方言也就是形變 神不變的語言變種,本文說的Lisp均指Lisp的方言Common Lisp。另一個比較有名的方言是 Scheme,關于各個方言的特點,<Land Of Lisp>里也給了一個圖片:

            imgs/dialect.png

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

            要學習Lisp,首先就是選擇方言。然后最重要的就是選擇一個編譯器實現。世界上知名的有 十幾種實現(也許更多)。一些商業版本非常強大,甚至能編譯出很小的本地代碼執行文件 ,不過價格也不菲。當然也有很多開源免費的實現,例如CLISP、SBCL。我選用的是SBCL。

            SBCL交互式命令行不支持括號匹配,甚至沒有輸入歷史。要實現這兩個功能,可以裝一個 lisp工具:linedit。在lisp的世界中,要獲得一個lisp的庫實在不是件方便的事。尤其是 這些免費的編譯器實現,并不像有些語言一樣,直接隨編譯器帶個幾十M的庫。

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

            此外,在lisp的世界里,寫出來的程序不再是跨OS。OS的差異由編譯器實現來解決。但是, 寫lisp程序卻需要考慮跨編譯器實現(egg hurt)。這也是個無比傷神的事,比跨OS更傷 神。因為OS就那么幾個,但lisp的編譯器實現,流行的也有好幾個。

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

            博客客戶端如何實現

            像我們這種基本沒接觸過Web開發的人,可能完全沒有思路去實現一個博客客戶端。事實上 實現起來非常簡單。

            使用過其他博客客戶端(例如Windows Live writer)的人肯定知道metaweblog API,在配 置客戶端的時候需要填入。例如CPPBLOG的這個地址就是 http://www.shnenglu.com/kevinlynx/services/metaweblog.aspx。這個頁面展示了一些API 說明。這些API就是博客客戶端和服務器進行操作通信的接口。意思是說,服務器端提供這 這些接口,我們的客戶端調用這些接口即可。例如:

            blogger.deletePost,調用該接口即可刪除一篇博客文章
            

            但是客戶端如何調用到這個接口呢?這需要通過一種新的技術(或者說標準),即 xml rpc 。rpc大家應該清楚,xml rpc其實說白了, 就是把接口調用的細則塞進 http 請求發給web服務器,服務器接收請求完成操作后再把結果以http回應的形式丟給客戶端, 即完成了一次接口調用

            至于http請求回應的細則就不提了,無非就是一些特殊格式的數據,通過tcp連接與服務器 交互這些數據。

            所以,基本上,整個過程還是非常簡單。如何來將調用細節塞進http請求,則是以xml rpc 標準來做,其格式正好是xml格式。舉個例子吧:

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

            當然這部分數據之前就是若干http請求的數據。服務器回應也是以xml格式組織:

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

            我們的博客客戶端所要做的,就是把這些博客發布相關的操作封裝起來提供給使用者。底層 實現主要包括http請求、xml-rpc的組織等。何況,這兩部分在各個語言里都有大量的庫存 在,lisp自然也有。

            我這里直接選取了lisp的一個xml-rpc庫:s-xml-rpc,基本上百來行代碼就可以把各個功 能跑一遍。例如以下lisp代碼就實現了通過s-xml-rpc刪除CPPBLOG的一篇文章:

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

            發布博客也很簡單,根據metaweblog API接口的說明,發布博客時需要填充一個結構體。但 主要涉及到的數據僅包括:文章內容、文章標題、文章分類(可選):

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

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

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

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

            完整實現方案

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

            我的打算是先用其他工具對文章做排版處理,最后導出為html格式。因為CPPBLOG支持直接 發布一個html文件。然后在用這個lisp工具將整個文件作為博客文章內容發布。

            恰好公司最近打算用reStructureText(rst)格式來編輯文檔,作為熟悉手段,我決定拿這個 來練手。rst格式非常簡單,同wiki命令很相似。在vim里編輯該文件非常合適,因為默認支 持。見圖:

            imgs/rst.png

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

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

            但是如果文章中出現了圖片,而圖片基本上在本地,轉成html后也是相對路徑。我需要我的 lisp writer(cl-writer)能自動掃描文章,發現有圖片的地方,就自動將圖片上傳。最惡心 的是上傳后還得替換圖片引用路徑。這個工作可以在rst格式上做,也可以在結果格式html 上做。通過xml解析庫解析html比直接解析rst格式更簡單,并且在擴展性上更好。

            最終這個html中圖片路徑替換工作只消耗了不到100行lisp代碼。這在很大程度上也依賴于 s-xml庫的接口設計。

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

            (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

            別指望我發布的代碼能夠讓你一鍵在你的博客上留下"this is a test",你甚至別指望它能 能夠工作。但如果你本來就是一個資深的lisper,或者雖然不是lisper但卻執意想看看結果 。這里我就簡要說說如何讓這些代碼歡樂起來:

            1. OS Ubuntu10.04,下載安裝SBCL,不會有問題;

            2. 下載安裝quicklisp,官方文檔hand by hand,簡單不會有問題;

            3. SBCL交互環境中使用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,但我不能保證成功,配置文件則 要復雜點:

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

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

            下載代碼

            最后,終于敲完這篇文章,我需要通過以下步驟來發表它:

            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 閱讀(16875) | 評論 (8)編輯 收藏

            飛秋lua版:luafeiq0.1.0發布

            繼上次搗鼓出了飛秋的群聊協議后,鑒于年底沒啥事情做,就用lua寫了個簡單的協議兼容的IM。本來開始讓

            另一個同事在iptux的基礎上修改的,結果大概是因為iptux的代碼不是那么容易修改,就不了了之了。這個

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

            收發,簡易的消息盒子(暫存未讀消息)。因為選的庫都是跨平臺的,所以很容易的luafeiq也是跨平臺的,

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

             

            之所以選用lua,一方面是想練練lua,另一方面則是因為開發效率。前段時間在android下寫了些java代碼

            用java寫代碼覺得甚為爽快(當然算不了完美)。這幾天寫了千把行的lua(也許有3K行,未統計過),

            感覺也不錯。綜合來說,這些高級語言的很多好用的語法特性,例如閉包(closure),垃圾回收,都提高

            了不少寫代碼的速度。當然,lua于我而言也算不上完美的語言。例如我經常因為變量敲錯字母,而在運行時

            才暴露nil錯誤。這也許可以通過諸如IDE之類的工具在寫代碼的時候就給予提示。lua 在遇到一個符號時,

            默認地將其處理為全局的。關于這個語法特性早有人提出不爽,只能說大家設計的準則不一樣。(在我們

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

             

            再來談談實現過程中的一些瑣事。因為飛秋也算是IP messager協議的兼容實現,很多通信除了可以使用

            抓包軟件分析外,還可以直接通過IP messager的源碼來了解。所以,基礎通信協議的實現過程也比較

            簡單。飛秋與飛秋之間發送私聊消息是經過加密的。其加密過程也不簡單,更重要的是,我并不想浪費太

            多時間在這上面。后來發現其實可以通過上線消息里某個標志位表明自己不需要加密。這個標志就是消息頭

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

             

            發送私聊消息時,消息里會攜帶一個消息ID。這個ID可以通過任意算法生成,例如直接取time的值。接收到

            對方的消息時,需要取出該ID,然后加入回應消息。對方收到回應消息后,就知道自己發送成功。這個過程

            算是ip messager在UDP上做的消息可靠驗證,過程也比較簡單。

             

            群聊消息在之前提到過,是通過UDP多播實現。我們可以接收所有群的消息。如果之前已經處于某個群里,

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

            可以通過多播一個加入群的消息,然后就可以不請自來地在這個群里發言。詳細的消息值和實現都可以從

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

             

            在linux下接收windows上的飛秋消息,是需要做字符編碼轉換的。因為luafeiq使用IUP作為UI庫,IUP在

            linux下使用GTK作為底層實現,默認全部是UTF8編碼。luafeiq里我自己寫了個lua庫,用于編碼轉換。

             

            話說IUP作為一個UI庫,還是比較不錯的。正如其介紹文檔里所說,學習曲線低,基本上看一會文檔,就可以

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

            時間,各種奇怪的沒多大意義的錯誤信息。后來換成3.2版本,居然一下子就和諧了,無限怨念。

             

            luafeiq目前放在googlecode的版本,可以說是一個很不負責任的版本。早上我才剛把字符編碼轉換的代碼

            調試好。今天已經請假,家里就一臺電腦,也就測試不了這個字符編碼轉換是否真的能正常工作。我在

            windows下dump了些字符,看上去能正常功能。明天得回老家過春節,上不了網,索性就提前發布了。

             

            luafeiq項目地址:http://code.google.com/p/luafeiq/

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

            逆向思路:破解飛秋群聊協議

            題外

            飛秋是一款局域網內的IM軟件,界面類似QQ,實現上與飛鴿(IP message)有點淵源,免費,不開源。

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

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

            所以,就想寫個協議兼容的程序(或者說改個東西)來收取群信息。

             

            準備

            我本身并不擅長逆向工程,破解什么的純碎業余。在GOOGLE/BAIDU之后發現并沒有前人留下的成果。

            使用抓包程序,以及綜合網絡上的信息,還是可以得出不少有用的信息:

            # 飛秋兼容了飛鴿的協議,其協議格式基本上基于文本形式,各個內容之間使用冒號作為分隔,例如:

            1:100:user:pcname:32:message_body

            # 飛秋在私聊情況下的消息內容是沒有加密的,但群聊信息加密了,解密這個內容也是我的目標所在

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

               的成員,也會收到群消息

            有用的文章: 《局域網內實現飛鴿欺騙》《飛鴿傳書數據加密分析》(個人感覺沒有任何實質內容,而

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

            以及linux下的iptux(另一個兼容飛鴿的局域網IM)代碼,都是對接下來的破解有益的。

             

            破解

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

            更多的耐心、運氣、程序之外的思考。如前所做的準備,尤其重要。

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

             

            STEP 1 找入手點

            在開始面對一大推匯編代碼時,我們需要一個最接近目標的點。獲取這個點根據目標的不同而不同。例如,

            這里主要是針對網絡數據的解密。所以,最直接的就是去找這些網絡數據。當然,根據具體程序的表現,也

            可以從其他點入手,例如飛秋收到群消息后會在任務欄閃爍圖標,也可以從這個功能逆向過去。

             

            因為飛秋使用UDP協議,所以我們可以在recvfrom函數下斷點(bp recvfrom)。因為接收UDP包的接口

            可能還有WSARecvFrom,甚至winsock1.0中的recvfrom,所以最好都下斷點。另一臺機器發送群消息,

            程序在winsock1.0里的recvfrom斷下來:

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

            這個不是我們需要的,我們需要根據這個點獲得用戶層代碼,這將是整個破解過程的起點。所以,OD中

            ALT+K查看調用堆棧,然后跳到調用recvfrom的函數處:

            00490890  /$  56            push    esi                              ;  接收數據的函數入口
            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已經將調用recvfrom時傳入參數的指令標記出來了。中文注釋是我分析時加的。recvfrom里傳入

            的接收緩存,是我們應該關注的。如果能跟進這個緩存,假設程序的流程比較簡單:接收了數據,然后在某個

            地方直接解密,不管它的加密方式是什么,只要能找到這個緩存數據從加密變為解密的地方,對于整個破解而言,

            都算是邁進了一大步。

            于是,在00490890(上面找到的函數入口)下斷點,準備跟進接收緩存。注意:在OD里調試跟在vc里不一樣,

            跳到調用堆棧里的某個函數,寄存器依然是當前的值。所以需要重新跟。

             

            STEP 2 內存斷點

            F9讓程序繼續運行,再次在另一臺機器上發送群消息。這回程序在00490890處斷下,然后跟接收緩存:

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

            接收緩存Buffer的值為0011F4CC,如前所述,我們要跟進這個地址指向的內存的變化情況。F8單步運行到

            recvfrom后,也就是接收了網絡數據后,查看內存內容

            (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  [悸謁?糦鼭ё

            內容開頭正是飛秋的協議頭,未加密,不過沒多大用。根據之前獲取的飛秋協議,可知,在0011F51E

            處就是聊天內容的密文。

            很自然地,為了監視這段內存的變化情況,在該位置下內存訪問斷點(右擊數據區即可看到下斷點的選項)。

            F9繼續走,然后馬上斷下來:

            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

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

            附近的代碼,沒什么可疑。通過下內存訪問斷點的思路,似乎顯得荊棘叢生。

             

            STEP 3 靠近目標

            不妨冷靜下來思考,如果沒有直接的路,我們可能需要悲慘地大海撈針。在寫一個網絡程序時,網絡底層

            收到數據包,無非要么直接進行邏輯處理,要么緩存到一個邏輯處理隊列里稍后處理。后者雖然對于程序員

            而言是個好方法,但是因為跨了線程,又涉及到隊列緩存,對于逆向而言,絕對是悲劇。

             

            但是如果使用了前者呢?對于一個網絡客戶端程序而言,也許直接進行邏輯處理才是最KISS的方法。(這種猜測

            的破解方式,絕對需要運氣。)所以,如果是直接進行處理,那么在接收到網絡數據附近,必然就有解密函數。

            所以,不妨順著收包函數附近隨意瀏覽一番。(但不要走進太深的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                             ;  接收網絡數據

            0048FE10函數調用了剛才發現的收包函數。這個函數在收完數據后,不久就調用了另一個函數:

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

            進到004900D0函數,發現這個函數真TMD巨大。隨意瀏覽之,發現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,恩,消息頭里有這個字符串標識。估計是在做些消息頭的邏輯操作。這個函數太長,里面還有若干call,

            可謂頭大。所以說,代碼寫得丑,可讀性差,對于防破解還是頗有益處的。跳回到0048FE43,發現當前

            函數基本完了。

             

            于是往上看,來到調用這個函數的地方:

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

            回顧下,我們有函數A接收網絡數據,有函數B調用A,現在回到了C,來到了調用B的地方0050F647。C函數

            也很巨大,直接往下瀏覽,會發現一些switch語句:

            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

            往后瀏覽下這個switch…case,發現非常之大,這個函數也因此非常巨大。不妨猜測這個是根據不同消息做不同

            邏輯處理的地方。這個想法正是給予我們靈感的關鍵。

             

            群聊消息必然也有個類型,通過之前OD獲取到的網絡數據(或者截取網絡封包所得),群聊消息的類型為:4194339

            (16進制400023H),去掉高位,也就是23H。仔細地對比每一個case,果然發現了一段處理代碼:

            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

            這個代碼之下的處理也有不少代碼。在不涉及到更多細節之前,我們可以大膽地將注意力放在接下來的call上。從這個case

            往下,第一個call為:

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

             

            STEP 4 多嘗試

            有懷疑,就用事實來證明。果斷地在005127E6處下斷點。然后發群消息,程序斷下來。因為這個函數壓入了

            eax作為參數,且對ecx做了賦值:

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

            在調用一個函數前對ecx做賦值,一般都是C++成員函數調用。eax作為一個參數,非常值得關注,果斷查看eax

            指向的內存值:

            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ひ

            有用戶名、機器名、發送者MAC地址,這么多可疑信息。完全可以猜測,eax傳入的是一個結構體地址,

            當然對象地址也可以,反正是個復雜數據結構。更重要的,在這塊內存往下不遠處,果斷地發現了從

            網絡接收到的加密的聊天內容:

            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直接步過0051CA90函數。任務欄開始出現閃爍的圖標(雖然沒有閃),上面的內存數據變了:

            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正是我發的內容。

             

            STEP 5 縮小范圍

            實際上走到這里,憑借運氣和程序編寫的常識,已經找到關鍵點。不過看來0051CA90這個函數做的事情

            有點多,除了解密內容似乎還有UI上的一些處理(例如那個閃爍的任務欄圖標)。所以,我們要做的是,進一步

            跟進,找到關鍵點。

             

            現在縮小范圍要容易得多,因為我們得到了一個會變化的內存地址:00123670。只需要F8一步一步地

            運行代碼,一旦發現內存內容改變,則可以進一步進如,從而找到關鍵call。具體過程我就不給了,大概為:

            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                             ; 跟進

             

            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                                  ;  數據長度
            00490DC1  |.  8D4424 20     lea     eax, dword ptr [esp+20]
            00490DC5  |.  57            push    edi                                  ;  輸出緩存
            00490DC6  |.  50            push    eax                                  ;  輸入緩存(加密內容)
            00490DC7  |.  8D4C24 20     lea     ecx, dword ptr [esp+20]
            00490DCB  |.  E8 5049F7FF   call    00405720                             ;  最終解密函數

             

            00405720函數內的實現基本上全是數據操作指令。加解密算法無非就是搗鼓這些數據,所以當我進到

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

             

            STEP 6 解密

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

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

            的數據。在了解了這些情況后,大可以將這段匯編復制出來直接作為C++內嵌匯編代碼使用。

             

            不過,這里我想到了更簡單的方式。因為我注意到飛秋和飛鴿在實現上有著不解之緣,而且我琢磨著作者也不會

            為了一個加解密不太重要的應用場合而去整些高深的算法。我想到,飛秋也許會直接使用飛鴿里的加解密代碼!

             

            在IP message的源碼里,有blowfish加密算法的實現,我們來看接口:

            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);
            };

            從接口實現來說算是簡潔漂亮友好和諧。我也用Decrypt這個函數的參數比對了上面沒找到的那個call(00405720),

            因為這里只是懷疑這個call就是這里的Decrypt,但并沒有確切的證據。不過,對比下他們的參數就可以非常肯定了:

            00490DB0  |.  6A 00         push    0           ;參數IV
            00490DB2  |.  83E1 03       and     ecx, 3
            00490DB5  |.  6A 22         push    22        ;參數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                                  ; 參數 數據長度
            00490DC1  |.  8D4424 20     lea     eax, dword ptr [esp+20]
            00490DC5  |.  57            push    edi                                  ;  參數輸出緩存
            00490DC6  |.  50            push    eax                                  ; 參數輸入緩存(加密內容)
            00490DC7  |.  8D4C24 20     lea     ecx, dword ptr [esp+20]
            00490DCB  |.  E8 5049F7FF   call    00405720                             ;  最終解密函數

            最重要的,是參數mode。Decrypt默認參數mode是BF_CBC|BF_PKCS5的位組合,結果,恰好為22!

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

             

            STEP 7 密鑰

            查看CBlowFish的使用,在解密前需要初始化,大概就是傳入密鑰之類。如果我們上面的猜測沒有錯,那么我們

            從網絡上取得的數據,然后取得密鑰,直接使用blowfish的源碼,就可以解密出消息內容。

             

            接下來的關鍵就是,找到這個密鑰。關于這個密鑰,之前在飛秋的配置文件FeiqCfg.xml里繞了很久的圈子,因為

            發現加入一個群的時候,這個文件里就會多出一項很長的16進制字符串。也一度猜測密鑰就是保存在這個字符串的

            某個偏移里。接下來會讓人大跌眼鏡。

             

            因為CBlowFish這個類確實簡單,使用它的最簡單方式就是直接創建局部對象,然后傳入key和keysize,即可完成

            初始化。在之前展示的思路里,我也一度先去嘗試最簡單的方法。對于C++局部對象的創建,有個顯著特征就是

            mov ecx, dword ptr [esp+xxx],也就是往ecx里寫入一個棧地址。而且可以肯定的是,這個構造代碼,必然發生

            于call 00405720前面不遠處,往上跟進:

            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                             

            一個壓入兩個參數的函數調用,對比CBlowFish的構造函數,剛好是2個參數。跟進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

             

            又是可愛的立即數!48H、1000H,這種特別的立即數總能讓人安心,對比CBlowFish構造函數實現:

            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處,正是構造CBlowFish對象的地方,而構造的參數,正是我們魂牽夢縈的解密密鑰:

             

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

             

            在此處下斷點,發送群消息,程序斷下來,看看密鑰究竟是什么。如果它確實是FeiqCfg.xml里的某個值,

            那么我們還要進一步跟這個值具體在哪個配置項里。看看吧,密鑰edx:

             

            edx=00123644, (ASCII "88AE1DD466FD")

             

             

            TM的密鑰居然是發送者的MAC地址!當我看到這個的時候我幾乎快摔倒地上。如果飛秋使用一個MAC地址

            作為密鑰,那么這意味著:通過自己寫的程序,可以取得局域網內其他群里的聊天內容!這個實在太邪惡了。

            上回抓包的時候,雖然看不到內容,但可以看到美術、策劃在群里聊的無比歡樂。這回有喜感了。

             

            STEP END 可略

            看看時間,悲劇地發現整篇文章花了接近3個小時才寫完。此刻我正躊躇要不要把代碼發上來,但轉念一想

            最后那個STEP的發現實在讓人蛋疼,所以還是算了。打算稍微封裝下,然后使用這份代碼在iptux 上改改包裝

            個界面,目的就算達成了。相信瀏覽完整篇文章,寫出自己的代碼不是什么大問題。

             

            其實我大可以直接給結論,但是我依然樂于分享過程和思路。一來算是自我總結記錄(每次拿起OD,總是快捷

            鍵一路忘);二來也給有心人一個指引。

             

            最后,對這種東西還是有必要出個免責聲明:根據本文章所造成的一切后果與文章作者無關。為了不糟蹋我這3個

            小時的時間,轉載麻煩注明出處。

            PS,最后回顧下結論,其實發現這個解密非常非常簡單。你說如果直接給盧本陶(飛秋作者)發封郵件,他會不

            會直接告訴我?

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

            一段tricky codes:函數調用的那些底層細節


            有一天,被同事問到了下面這段代碼,就簡單分析了一下,發覺還有點意思:

            __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函數的大致作用,就是調用傳遞進去的函數print_str,并將參數"a string"傳遞給目標
            函數。

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

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

            了解了以上常識后,再看這段代碼,其本質無非就是利用了這些規則,在代碼段跳來跳去。
            我們來逐步分析一下:

            在調用call函數的地方,大概的代碼為:

             

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

            call:
            // 此時堆棧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并跳轉,也就跳轉到print_str函數的入口,此時堆棧
                
            // stack: end_label, a1, a2, 

            callee(print_str):

             無視函數內容

            ret 
            // print_str返回,此時正常情況下,堆棧stack: end_label, a1, a2, 
             
            // 取出end_label并跳轉,stack: a1, a2, 

             

            那么當callee結束時,則跳轉回caller函數中。不過,如過你所見,此時堆棧中還保留著再
            調用call函數時傳入的參數:stack: a1, a2, ...,所以,DEBUG模式下,VS就會提示你堆
            棧不平衡。這里簡單的處理就是手動來進行堆棧平衡:

             

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

             

            傳入了多少個參數,就得相應地改變esp的值。

            話說距離上篇博客都有半年了,自己都不知道時間晃得如此之快。最近業余折騰了下android開發
            一不小心就跨年了。
             

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

            網游中的玩家移動

                 摘要: MMORPG中,玩家的移動主要邏輯都放在客戶端進行,包括自動尋路和響應玩家的操作,服務
            器在這里擔當一個被動角色。但是服務器端的玩家數據卻是真正的被其他邏輯模塊參考的數
            據。  閱讀全文

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

            僅列出標題
            共12頁: First 2 3 4 5 6 7 8 9 10 Last 
            日本欧美国产精品第一页久久| 久久激情五月丁香伊人| 久久99精品久久久大学生| 久久国产精品77777| 欧洲性大片xxxxx久久久| 伊人久久久AV老熟妇色| 91久久精品国产成人久久| 国产成人综合久久精品红| 久久91综合国产91久久精品| 亚洲七七久久精品中文国产 | 久久精品无码一区二区app| 天天躁日日躁狠狠久久| 久久久99精品一区二区| 久久人妻少妇嫩草AV无码专区 | 久久久久99精品成人片直播| 久久精品中文字幕一区| 精品久久香蕉国产线看观看亚洲| 三级韩国一区久久二区综合| 国产成人无码精品久久久久免费 | 久久精品这里只有精99品| 精品久久久久久国产潘金莲| 免费精品久久天干天干| 精品综合久久久久久88小说 | 亚洲乱码日产精品a级毛片久久| 久久精品国产亚洲av麻豆色欲| 久久精品桃花综合| 久久男人中文字幕资源站| 国产精品成人无码久久久久久 | 伊人久久大香线蕉综合影院首页| 理论片午午伦夜理片久久| 国产日韩久久免费影院| 亚洲嫩草影院久久精品| 久久久久久综合一区中文字幕| AV色综合久久天堂AV色综合在| 亚洲精品高清国产一线久久| 久久SE精品一区二区| 2021国内久久精品| 综合网日日天干夜夜久久| 亚洲国产精品无码久久久秋霞2 | 色狠狠久久综合网| 久久久亚洲AV波多野结衣|