VC编写Demo Scene的一些可能技巧

本来也不是专门为了写这篇文章,只不过觉得已这样的形式发表比较合适。同时好久都没有写过教程了,以往都以简单的发表作品或者通报一些事情为主。总的来说这篇文章还是有点参考价值的,希望能有所帮助。

 注意:

为了最佳的阅读效果,请访问:http://www.csksoft.net/blog/post/demo_scene_tip1.html

转载注意:

请注明原文出处: http://www.csksoft.net/blog/post/demo_scene_tip1.html

 

Demo Scene我就不想再介绍了,对他还不了解或者听说过但不明白原理和背景的可以参考我以前写的一些文章:

Demo Scene:Principles,Techniques,and Tools (http://www.csksoft.net/blog/post/154.html)

模块音乐(Mod)的制作和使用,Demo程序的主体之一 (http://www.csksoft.net/blog/post/intro2track_music.html)

 

目前国内Demo Scene基本处在0起步的阶段,已经有了一些小团体打算去参加欧洲的比赛,但是还没有一定的规模。同时,对于制作这类程序网上也没有系统的资料。使得制作Demo Scene被看成一种高深的事情。

下面我就说说目前在Windows平台下,使用最常用的开发工具(Visual C++)如何来制作一个符合64kb demo的程序框架和常用技巧。当然这只是一些次要手法,最核心的还是3d引擎、mod音乐的设计。因为那些资料很好找。所以就不再涉及。

 

我将介绍下面几个方面的技术:

 

1.如何产生体积最小的程序

2.如何不使用C运行库开发程序

3.如何实现高速GDI绘图

4.对于NT5.0提供的LayeredWindow的使用--不规则窗体、窗口的AlphaBlend渲染、鼠标事件穿透

5.如何将所有数据(代码、图片等)整合在一个C文件中

6.其他的一些编译技巧

7.一个完整的示例程序代码

问题和需求

Scene Demo中有一个项目为4kb-intro 或者 64kb-intro。 他要求Demo的程序体积必须小于或者正好等于4/64kb。而往往正是这类Demo程序在国内流传最广。因为大家都认为那么小的体积能播放长时间的高品质3d动画和音乐是不可思议得。甚至有人将45分钟的demo动画看成是avi视频,45分钟的音乐算作44KHz采样的wav。计算出将他们压缩到64kb完全是不可能的(见farbrausch的作品: the product 中的说明字幕)。当然这只是忽悠外行的吓人话。其实写过游戏引擎的人都知道那只是通过实时渲染的到的,而音乐本身就是体积在12kb左右的mod音乐序列(见我以前写过的文章)。

 

目前很多机器都已安装最新版本的DirectX,而OpenGL是windows的默认库之一。这样Demo Scene设计者一般就不需要自己去编写基本的3d引擎。动画部分几个基本特效的代码不会超过30kb(这里假设开发者具有较高的设计素质),而一些复杂网格模型的纹理贴图即时采用bmp保存,也在100kb-300kb左右。加上mod音乐和其播放引擎。一个64kb DemoScence程序的原始体积一般应在600kb。而不是通过用等效为avi文件计算方法算出的几个G的天文数字。

 

不过,问题就产生了,实际程序体积只有64kb。在这600kb->64kb还是有相当距离的。如何尽可能的去减少这部分文件大小,以及其中伴随的一些技巧就是本文所要讨论的。

 

Some Tricks

对于减小程序代码体积,自然对coding的技巧有一定要求。不过这不是问题所在。大家都知道使用VC编译产生的程序,即使就写了句printf("HelloWorld");,也会产生出100kb以上的代码。但是实际上这行语句的有效代码只是:

WriteFile(StdOut,  "Hello World",12,NULL,NULL);

WriteFile只是句API call, 实际上对应汇编大致为(这里只是说明性描述):

push NULL

push NULL

push 12

push offset "Hello World"

push StdOut

call WriteFile

就这么几行汇编指令,大致也就几十字节的数据,但VC却占用了大量的体积。而那些多余的文件数据主要是下面这些内容:

1. C运行库

这应该是最主要的因素,程序中会编译近大部分的c函数库的代码信息。同时,在程序的开始执行点到逻辑上认为起始位置:main函数之间也填充了大块的C运行库代码完成初始化工作(初始化堆数据、获取命令行数据)。对于完成通常编程任务来说,这些代码是十分必要的。但是对于一个需要尽可能小体积,且具有足够经验的Demo Scene开发者来说,这些代码绝对是鸡肋。

因此,减少程序体积的第一要务就是将C运行库完全从程序代码中剥离。不过要实现这个需要满足几个前提:

a.程序不能使用C运行库,这是当然的。不过有人会问一些很常用的函数,诸如printf没有了该怎么办。回答只有是:自己使用等效的API实现。不过后文也会介绍一些办法

b.尽量不要使用C++语言,原因是对于class的一些操作,诸如析构操作。new/delete运算符。这些本该是语言特性的语句,实际上在编译时会去调用c运行库来完成。

c.不要使用tchar.h

d.关闭VC后续版本提供的堆栈安全检查、异常处理等特性

e.完全采用Win32 API

对于很多人来说,要满足这些条件已经无法正常编写程序了。可能这也是Demo Scene的一个门槛。这里有一个变通办法,就是采用微软提供的精简版C运行库(LIBCTINY.LIB)或者使用ATL/WTL中的精简版C运行库。也能大幅减小体积。(实际上,在kernel.dll和ntdll.dll中也提供了C运行库的API接口)

在符合上述条件后,就能大胆的将C运行库去除。具体办法就是在链接设置中取消默认库,或者用下面语句:

#pragma comment(linker,  "/nodefaultlib")

此时,不会有任何的C运行库被编译进程序,但是基本的windows API还是需要链接的。因此起码需要kernel32.lib。

#pragma comment(lib,  "kernel32.lib")

接下来可以按照需要添加相关的lib或者用LoadLibrary自行加载其他库。具体相信也不需要我废话了。

不过需要注意的是几个特列。位于Winnt.h中有如下定义:

# define RtlFillMemory(Destination,Length,Fill) memset((Destination),(Fill),(Length))
# define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))

相信MS这样做无非是考虑到运行效率和debug的需要。但是这样也给我们的工作造成了麻烦。如果在程序中直接或间接的使用了这2个函数(还有其它情况)的话,仍旧会被linker告知symbol _memset不存在。最直接的办法可以去修改这个winnt.h。但相信这是个十分愚蠢的做法。因此,推荐的办法是先undefine这些定义,再重新import。

#undef memset
#undef RtlFillMemory
extern  "C" NTSYSAPI BOOL NTAPI
RtlFillMemory ( VOID *Source1,DWORD Source2,BYTE Fill );
# define memset(Destination,Fill,Length) RtlFillMemory((Destination),(Length),(Fill))

其他类似的情况也可以这样处理,同时大家也可以开动智慧将部分C运行库用kernel32.dll或者ntdll.dll中现成的等价函数替换。这样对于暂时不习惯完全采用WINAPI编写的开发者来说能带来些便利。

到目前为止,在代码逻辑上已经将C运行库从程序剥离了,但实际上编译还是不会通过。原因在于目前程序的真正起始位置还不是int main()或者int WinMain(...)这些。在执行这些逻辑上的开始位置前,还有不少的C运行库Wrapper。

要将这个Wrapper去除,直接办法就是在link选项中修改入口地址到目前的main(WinMain)函数。或者用等效语句:

#pragma comment(linker,  "/ENTRY:MyMain"

此时,如果程序中有MyMain这个函数,那么他将真正作为程序的入口点(OEP)。不过要注意的是,作为OEP的函数不能带参数。因为传统的main函数或者WinMain中那些命令行信息的参数,都是由之前的C运行库Wrapper获取提供的,而直接从OEP启动时候,是没有参数提供给程序的。这样将造成堆栈的不平衡(但实际影响不是很大)。

在做到这一步后,就可以尝试编译程序。以VC中创建的SDK的helloworld示例程序为例。经过目前的操作,产生的程序应该在2kb左右或更小(作者并未测试,只是估计值)。不过这里要注意的是,目前的程序有一个问题:进程无法结束。这是因为在原始的C运行库Wrapper中,执行完了WinMain等程序,会调用ExitProcess()终止进程。因此,在我们的新OEP程序的适当地方,加入此语句即可。

对于原先WinMain等函数中参数的获取

要获取诸如命令行参数或者HINSTANCE值,其实可以调用相应的API实现。对于命令行,可以调用函数:

GetCommandLine( void); 

对于HINSTANCE,可以调用

GetModuleHandle(NULL);

其他参数这里就不列举了,可以查阅相关文档或者反汇编原始的C Wrapper分析。

2.段合并问题

段(Section)是一个PE文件格式中的术语,具体可以参照MSDN中的一篇很完善的PE格式介绍文章"An In-Depth Look into the Win32 Portable Executable File Format"(http://msdn2.microsoft.com/en-us/magazine/cc301805.aspx),对于采用VC生成的程序文件&

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值