Category Archives: TTTech

T_T Tech…

Makefile 自动生成头文件的依赖关系

最近在看一本书《Windows游戏编程大师技巧》 (Tricks of Windows Game Programming Gurus). 第一章给出了一个打砖块小游戏的示例程序. 包括三个文件: blackbox.h, blackbox.cpp和freakout.cpp (600行代码, 对于Windows C++程序来说还好, 没有让我freak out…). blackbox.cpp封装了部分DirectDraw, 提供了一些更傻瓜化的初始化DirectDraw, 画点, 画方框的工具函数. blackbox.h包括了这些函数的声明. freakout.cpp引用了blackbox.h文件, 包括WinMain主函数和主要的游戏逻辑.

How to build?

和我一样还在使用N年前的垃圾电脑的看官们, 多半也不愿意用Visual Studio来挑战自己的耐心. 但是所以让我们选择小米加步枪: GCC (MinGW) + Makefile. 写个简单的makefile例如:

all:    freakout.exe
 
freakout.exe: freakout.o blackbox.o
    g++ freakout.o blackbox.o -o stupid.exe -L D:gamedevdx81sdkDXFDXSDKlib -lddraw -mwindows
 
freakout.o: freakout.cpp <strong><span style="color: #ff0000;">blackbox.h</span></strong>
    g++ -c freakout.cpp -o freakout.o -I D:gamedevdx81sdkDXFDXSDKinclude
 
blackbox.o: blackbox.cpp <strong><span style="color: #ff0000;">blackbox.h</span></strong>
    g++ -c blackbox.cpp -o blackbox.o -I D:gamedevdx81sdkDXFDXSDKinclude

Problem?

上面的代码当然有很多问题, 比如”-L <DX lib path>”, “-I <DX inc path>”可以被定义在类似$(LIB), $(INC)的变量里面, 比如我没有按照makefile的习惯写一个clean目标清理生成的东东…不过我的重点是在红色高亮的blackbox.h: 两个.o文件都需要依赖于blackbox.h.

假设我们修改freakout.cpp引用更多的头文件, 每加一条#include “somefile.h”指令, 就需要相应地在makefile里为freakout.o增加一个依赖项somefile.h.

假设我们修改stupid.h文件, 让stupid.h也引用一个头文件someotherfile.h, 那么我们也需要相应地在makefile里为所有依赖stupid.h的.o文件 (在这个例子中是freakout.o和blackbox.o) 增加依赖项someotherfile.h.

所以问题是: 在.cpp和.h被修改的情况下, 怎么维护.o目标文件和.h头文件的依赖关系.

Solution – 自动生成依赖关系

Google一下”makefile 头文件 依赖”会发现大多数编译器都提供了一个选项生成.o目标文件所依赖的文件列表. 比如GCC的”-MM”选项. 运行GCC –MM freakout.cpp blackbox.cpp <库文件和头文件选项>得到输出:

freakout.o: freakout.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h
blackbox.o: blackbox.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h

所以一个简单的处理依赖关系的办法是把这些编译器生成的依赖关系写入一个文件里, 然后在makefile中用include指令包含这个文件:

CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:gamedevdx81sdkDXFDXSDKlib -mwindows -l ddraw
INC = -I D:gamedevdx81sdkDXFDXSDKinclude
 
all: freakout.exe
 
freakout.exe: ${OBJ}
    ${CPP} ${OBJ} -o freakout.exe ${LIB}
 
<span style="color: #ff0000;"><strong>include depend</strong></span>
 
# note the symbols: $&lt; and '$@
%.o: %.cpp
    ${CPP} -c $&lt; -o $@ ${INC}
 
# generate depend file
<span style="color: #ff0000;"><strong>depend:</strong></span>
    <span style="color: #ff0000;"><strong>${CPP} -MM ${OBJ:.o=.cpp} ${INC} &gt; depend</strong></span>

注意红色的部分, depend是GCC生成的包含了依赖关系的文件 (也注意上面的makefile中有”$<”, “$@”这种perl风格的bt匹配字符…) . 有了这样的makefile, 我们就能用两个步骤来build项目:

1) 在需要更新依赖关系的时候 (比如在某个文件里多加了一条#include指令) 运行make depend.

2) 运行make.

Makefile Auto Dependency – 只运行一次make

懒惰是程序员的一大美德, 所以GNU make的手册里提供了一个运行一次make就能更新所有依赖关系并且按照依赖关系build的办法 (http://www.gnu.org/software/make/manual/make.html#Automatic-Prerequisites): 与整体引入一个depend目标不同, 我们为每一个.o/.cpp文件引入一个.d依赖关系文件. 依赖关系文件由GCC的”-MM”选项生成, 并且在GCC输出的基础上把自己本身加入到目标列表中. 比如:

# freakout.d
freakout.o <strong><span style="color: #ff0000;">freakout.d</span></strong>: freakout.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h
 
# blackbox.d
blackbox.o <strong><span style="color: #ff0000;">blackbox.d</span></strong>: blackbox.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h

注意红色的部份: .d文件被加入到了目标列表, 依赖相关的.h和.cpp文件. 那么怎样自动生成这些.d文件呢? 类似的, makefile:

CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:gamedevdx81sdkDXFDXSDKlib -mwindows -l ddraw
INC = -I D:gamedevdx81sdkDXFDXSDKinclude
 
all: freakout.exe
 
freakout.exe: ${OBJ}
    ${CPP} ${OBJ} -o freakout.exe ${LIB}
 
<span style="color: #ff0000;"><strong>include ${OBJ:.o=.d}</strong></span>
 
%.o: %.cpp
    ${CPP} -c $&lt; -o $@ ${INC}
 
<strong><span style="color: #ff0000;">%.d: %.cpp</span></strong>
    rm -f $@ &amp; 
    ${CPP} -MM $&lt; ${INC} &gt; $@.$$ &amp; 
    insertdfile.exe $&lt; $@.$$ &gt; $@ &amp; 
    rm -f $@.$$

这个makefile和上一节给出的makefile大致差不多, 不同的是两个红色的部分: 第一部分我们用include指令include相关的所有.d文件; 第二部分定义了生成.d文件的规则 (又是$@, $<符号…): 第一行首先删除原有的.d文件; 第二行运行g++ -MM生成依赖关系写入一个临时文件($@.$$)里; 第三行把.d文件加入到第二行生成的依赖关系中并把最终结果写到.d文件中; 第四行删除临时文件.

P.S. insertdfile.exe是我自己写的一个小程序, 用来把.d文件加入到第二行生成的依赖关系中, 例如把”blackbox.o: blackbox.cpp blackbox.h”转换为”blackbox.o blackbox.d: blackbox.cpp blackbox.h”. 我为什么要自己写一个字符串替换的程序? 因为我没有装sed工具…如果有, 当然用官方推荐的办法, 把第三行换成神奇的符咒: sed ‘s,($*).o[ :]*,1.o $@ : ,g’ < $@.$$ > $@ .

好了, 运行make:

1) make尝试去包含blackbox.d和freakout.d文件, 没有发现, 所以检查有没有规则可以生成这些.d文件, 发现我们%.d: %.cpp的这条规则, 于是运行这条规则生成所有的.d文件并且包含进来.
2) 按照正常规则生成.o文件, 然后生成.exe.

然后我们做一些修改, 比如增加一个stupid.h文件, 然后修改blackbox.h文件以包含stupid.h. 运行make:

1) make把第一个目标all加入到需要生成的目标列表中.
2) make包含blackbox.d和freakout.d文件. 注意在这个过程中这两个.d文件也会被加入到make的需要生成的目标列表中 – 因为可能有规则能更新这两个.d文件.
3) 根据blackbox.d和freakout.d包含的规则: .d文件 (已经被入到了make的需要生成目标列表中) 需要被更新, 于是相应地运行%.d: %.cpp规则更新.d文件: 新加入的 stupid.h文件被加入到两个.d文件的依赖项中.
4) make发现包含的两个.d文件在第2)步中都被更新, 于是重新包含这两个.d文件.
5) make根据在第2)步生成在第3)步被包含进来的规则, 发现stupid.h的日期比两个.o文件的日期都要新, 于是重新生成.o文件.
6) 根据规则生成.exe.

刺猬战争 Hedgewars

又大半年没有更新blog了…
昨天发现一个类似”百战天虫”的开源游戏Hedgewars, 支持很多操作系统 (windows/linux/BSD/…), 可以在http://www.hedgewars.org下载安装程序和源码 (Windows/Linux). 游戏里面的主角从虫子变成了刺猬 (单人模式里面貌似npc是水果人…), 也是一样的游戏套路, 满天乱扔东西…游戏是用C++和Pascal写成的, 用了Qt库和SDL (Simple DirectMedia Layer)系列库, 都是跨平台的东西.

下了一个0.9.9版, 在windows下面安装完之后, 运行没多久就出现一个错误说某个zh-CN.txt文件找不到…google一下, 删掉了locale目录下面所有的*zh-cn*文件, 游戏变成英文版, 不过能正常运行了. 总体来说感觉不错, 在我的破电脑上也能比较流畅的运行. 画面和百战天虫相比, 总体风格差不多, 一些细节还需要改进, 比如把炸弹扔到别人头上就看不见了…音乐还是有一定差距的, 不论是音质还是背景音乐的选择. 音效貌似是照搬百战天虫的音效. 操作方式和原来一样, 省的再去适应. 然后就是不仅支持网络对战, 竟然还有官方服务器…进去过后发现也有20多个人. orz…

作为一个爱岗敬业的IT民工, 体验一下安装版的游戏过后, 应该要自己build一个出来玩玩. 查了一下怎么build, 就只有在官网的FAQ里面有几句说明: 先下Qt, FreePascal, SDL系列库, 然后去源代码目录cmake + make搞定. 当然, 这是理想情况, 实际操作起来, 不会那么河蟹…我在windows下的步骤:

1) 先找到官网上列的库, 下载下来. 总共有200多MB的东西. 因为Qt很大 (100多MB), FreePascal也有35MB. 然后把安装程序都装上, 压缩包都解开. 安装Qt的时候会提示下载MinGW (GCC的windows移植版), 如果有, 就不必下了.

2) 设置PATH环境变量. 包括:
<CMake路径>bin – 能在cmd line直接下运行CMake
<MinGW路径>bin – 使CMake的选项-G “MinGW Makefiles”能工作
<Qt路径>bin – CMake Hedgewars的过程中需要用qmake等工具
<FreePascal路径>bini386-win32 – CMake Hedgewars的过程中需要用到FreePascal. 注意这个目录下面也有gcc的一系列工具, 所以最好把这个目录放到<MinGW路径>bin之后
<SDL路径>includeSDL – CMake Hedgewars的过程中需要SDL的头文件和库文件
<SDL_mixer路径>include – CMake Hedgewars的过程中需要SDL的头文件和库文件
<SDL路径>bin – (build生成的) Hedgewars.exe运行需要SDL的DLL
<SDL_mixer, SDL_net, SDL_ttf的路径>lib – 共3个路径, (build生成的) Hedgewars.exe运行需要的DLL

3) 进入cmd line到Hedgewars的源代码目录, 运行CMake . -G “MinGW Makefiles”, DONE. 选项-G指定生成makefile的generator. 这里用的MinGW.

第二步里加了很多路径到PATH中, 有三种类型的路径: 第一是为了使cmd line能找到并直接运行程序比如<CMake路径>bin; 第二是使CMake能找到需要的工具程序, 头文件和库文件比如<SDL_mixer路径>include (因为Hedgewars源代码里的的CMakeLists.txt文件包含一些搜索相应工具/库的”find_program”, “find_package”指令, 我不确定除了修改PATH以外还有其他什么更简单的方式让CMake能找到这些工具/库); 第三是使生成的Hedgewars.exe程序运行时能找到DLL比如<SDL路径>bin.

简单的引导程序

看了赵博的《Linux内核源代码完全注释》,读了潇前辈的《操作系统引导探究》《8259A保护模式下中断编程》,受益菲浅。想起N久前看到某期开发高手上的写自己的OS的文章,不由冲动起来,我要写一个~~~~~~引导程序:)。很简单的一个引导程序,仅仅打印几个字符而已,耗费了我几个小时,学到了很多东西。
首先建立实验环境。WinXP+VMWare(linux),用VMWare自带的模拟软驱的功能做了一个软驱镜像,ok了
然后用C写个工具,把引导程序写到软盘0扇区中。并标志最后两个字节为“55 AA”
在这篇日志里我并不打算详细说明系统启动和内存分布情况,强烈推荐《操作系统引导探究》,顺便勘误,图十中的引导区应为0x07c00到0x07dff,而原图误为到0x07cff :) 。同时补充一点,在IBM PC机中,中断向量表中每个表项占4字节,前两字节是中断处理程序段地址,另两字节存储偏移地址。另,参考了唐朔飞老师《计算机组成原理》一书,在硬向量方式中,IBM PC机中的中断表和潇前辈提到的JMP式中断表是很普遍的两种方式。只不过PC机采用前者(清华大学《IBM-PC汇编语言程序设计》)

Ok了,进入正题。先把C代码贴出来。写入引导扇区的工具

/* the program is used to write a boot program to the boot section of
the floopy
*/

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>

main(int argc,char* argv[])
{
int bootp,flp; /* the bootp point to the boot program,flp point to the flooy */
char buf[512]; /* data buffer */
if(argc==1)
{
printf(“Use the command like this: writetoboot bootfilenamen”);
return;
}

if((bootp=open(argv[1],O_RDONLY))==-1)
printf(“Boot file not existn”);
read(bootp,buf,510);
close(bootp);

buf[511]=0×55;
buf[512]=0xaa;

if((flp=open(“/dev/fd0″,O_WRONLY))==-1)
printf(“Read the floopy driver error,please check your diskn”);
write(flp,buf,512);
close(flp);
}

这个程序应该不难理解,主要运用了linux系统调用open read write。不明白的可以man一下看看。

接着是引导程序。最早的原始程序如下:

.text

entry start
start:
mov ax,cs !ffmsg是显示的欢迎信息,这个字符串放在了.text段里面。
mov es,ax

mov cx,#20
mov bp,#ffmsg
mov bx,#0×0007
mov ax,#0×1301
int 0×10
loop1:
jmp loop1

ffmsg:
.byte 13,10
.ascii “YY,I love you…”
.byte 13,10

用这个作为引导程序,结果是能引导(0扇区结尾是55 AA的都能引导-_-b),但是显示不出文字。这个程序主要是利用了0×10号bios中断调用。显示字符串。其中字符串地址为es:bp 而这个程序之所以错就是es段寄存器值错误。我原来以为ffmsg既然定义在.text段里,那么其段基址肯定就是cs代码段寄存器的值了,偏移地址可以通过ffmsg本身的名字获得,把cs赋给es就ok了。这样想的错误在于弄混了概念。程序中的段与段寄存器不是那么直观地对应的。
cs段寄存器和IP程序基数器联用,确定下一条指令的位置。但cs不一定指向存储这个程序的第一个内存单元。因此把cs的值赋给es的值显然是错误的。ffmsg只能表示待显示字符串首字符的偏移地址,在es地址错误的情况下。整体就错误了。
在这个基础上修改程序。很简单,系统引导的时候,引导程序会被加载到物理地址0x07c00处,所以把刚才的mov ax,cs指令换为mov ax,#0x07c00就成功了。这时es段寄存器指向了这个程序的首地址,bp指向正确的偏移地址。所以显示成功。
那么进一步考虑。CS寄存器的地址是多少呢?写程序验证

.text

entry start
start:
! yes,it works,but I want to see what is the value of CS
mov ax,#0x07c0
mov es,ax

mov cx,#20
mov bp,#ffmsg
mov bx,#0×0007
mov ax,#0×1301
int 0×10

mov dx,cs !从这开始,把cs段的值赋给dx寄存器。然后显示
add dh,#65
add dl,#65
mov ax,#0xb800
mov es,ax
seg es
mov [0],dh
seg es
mov [1],#0x1f
seg es
mov [2],dl
seg es
mov [3],#0x1f
loop1:
jmp loop1

ffmsg:
.byte 13,10
.ascii “YY,I love you…”
.byte 13,10

把cs段的值赋给dx寄存器。分别把低16位(dl)与高16位(dh)加上65(字符A的ascii码),然后送往显存(0xb8000处开始),打印出来,0x1f显示属性表示蓝底白字。这时候引导我们可以很清楚的看到,打印了两个A字符,说明cs段值为0。现在可以充分证明最早程序的错误所在。再进一步可以打印出IP的值,IP寄存器不可以直接访问,不过我们可以利用call指令使IP入栈的特性来取得IP。如下所示

.text

entry start
start:
! yes,it works,but I want to see what is the value of CS and IP
mov ax,#0x07c0
mov es,ax

mov cx,#20
mov bp,#ffmsg
mov bx,#0×0007
mov ax,#0×1301
int 0×10

mov dx,cs
add dh,#65
add dl,#65
mov ax,#0×9000 !把IP的值赋给BX寄存器
mov ss,ax !Set the stack sec pointer
call next1
next1:
pop bx !bx is the value of IP reg

mov ax,#0xb800
mov es,ax
seg es
mov [0],bh
seg es
mov [1],#0x1f
seg es
mov [2],bl
seg es
mov [3],#0x1f
loop1:
jmp loop1

ffmsg:
.byte 13,10
.ascii “YY,I love you…”
.byte 13,10

这次分别打印出bx寄存器的高位和低位,可以发现打印出了’|’与’#’两个字符,查一查对应的ascii码表,可以看出执行到next1时ip寄存器值为0x7c23。说明我们程序确实是从0x7c00处开始执行的。