Calling Convention

之前并没有很在乎这方面的事情,直到最近需要在某个bcb编译的程序中集成steam sdk。

故事是这样的,steam sdk提供的接口是c++代码,官方号称支持visual studio和gcc两种编译器。但我不想先做一层包装转换成c的接口再使用,所以打算强行在bcb中链接这些lib。

现在问题来了,由于c++的abi很混乱,微软又喜欢玩花活,一旦接口是c++的格式(class, virtual table)就很微妙。这就牵扯到了calling convention的事情,也就是函数调用约定。这件事情本来很简单,在c的时代只要解决两个问题就好:1、参数个数是不是固定的。2、参数是如何被放到堆栈中的。第二个问题无关痛痒,所以c规定参数一律是从右到左压入栈中,这样用的时候就可以从左到右依次弹出。

所以calling convention只和第一个问题有关,这样就诞生了两个关键字:

__cdecl:支持不固定个数的参数,调用方caller负责清理堆栈。好处是printf之类的东西可以实现,坏处是函数内部不能对堆栈玩花活,因为管理权在外面。

__stdcall:参数个数固定,被调用方callee负责清理堆栈。好处就是函数结尾直接用ret xx解决所有问题,坏处是参数个数固定,因为编译期就得决定ret之后的字节数。顺道说,COM中所有调用都是__stdcall。

目前为止一切还是有序的,直到link的时候,出现了name mangling的问题。M$和Borland使用了截然相反的方式来修饰函数名,具体可以参考《Using Visual C++ DLLs with C++Builder》中的Table A。还好双方都提供了解决方法,任选一种就可以生成合适的符号名,可以愉快的link。

即使link没有问题,问题也还远未解决,一旦开始使用c++的接口,run-time的问题就浮现出来。这就要提到M$的发明:

__thiscall:用于non-static member function,会把this放入ecx,再调用函数。好处就是this指针非常容易找到,调试方便。坏处就是……这是M$的发明,而非标准。还好时至今日很多编译器都支持了__thiscall,巨硬不再孤家寡人。

实际上bcb对this的解决方法非常简单,就是把它当作第一个函数的参数,照样传递进去即可。调试时候也不见得有多麻烦,而且如今编译器对代码的优化非常风骚,ecx在入口可能还有用,进入函数体之后几乎就完全无用了。

__fastcall:时代的眼泪,从名字看很拉风,但定义特别复杂。具体就是说把前几个较小的参数从寄存器里传入,而不是压入堆栈中。这样调用双方省去了很多进出栈的操作,故名fast。时至今日,这些开销似乎没什么人在乎了。

其他的调用约定我没怎么看就不说了,手头的情况只和后两种有关。这个bcb的程序诞生于近20年前,作者大概觉得函数调用开销是一件很重要的事情,所以默认全部函数都采用__fastcall。而steam sdk的c++接口全部用默认的__thiscall,运行的时候caller把参数往寄存器一丢了事,而callee还苦苦在ecx和堆栈中寻找,代码就跑飞了。

解决方案非常悲剧,对于每个class的每个non-static member function,都提供一个静态函数作为wrapper。内部帮他们把参数位置转换一下,然后再调用sdk中的实际代码。

用macro封装的效果如下所示(1个参数)

#define STEAM_API_CALL_WRAPPER1(ret, type, interface, index, var1)	\
ret STEAM_API_CALL type##_##interface(type * thisptr, var1) {	   	\
	asm { 						\
		MOV eax, [ebp + 4 * 3];	\
		push eax;				\
		MOV ecx, thisptr;		\
		MOV edx, [ecx];			\
		CALL [edx + 4 * index];	\
	} 							\
}
//

使用之前必须逐个定义,还得注意虚函数在虚表中的位置。不幸中的万幸是bcb和vs的虚表排列都是一样的。万幸中的不幸是,steam sdk中居然还有头文件和编译好的lib中虚表里函数顺序不一致的情况。大概是某个程序员手贱没事就调整函数的位置,又不肯重新编译sdk吧……

STEAM_API_CALL_WRAPPER1(bool, ISteamUserStats, SetAchievement, 7, const char *pchName);

经过这么多折腾之后,终于成功在bcb中链接运行了steam sdk。接下来发现还有callback的问题,steam要调用从bcb程序里传入的函数指针!解决方法和上面类似,callback函数也需要有一层wrapper……

只要对所有使用到的接口做上述处理,最终一切都和谐美好的运行起来,完全没有使用额外的dll,直接在bcb中使用了steam_api.dll。我研究了一下网上已有的实现,都是采用包装一个wrapper dll的方式。我这边的方法相对安全一些,否则wrapper可以被轻松的替换掉。当然咯,steam有提供额外的drm措施,简单换掉wrapper并不能移除steam绑定就是了。所以如果不是像我这样有强迫症的话,写个wrapper dll是最简单直接的方式。

结论是如果以后需要写什么对外发布的sdk,还是以c接口的形式提供最具有兼容性。

笔记本上使用LPT口的GBA烧录卡

最近怀旧病发作,入了一根PCI-Express转LPT(打印口)的转接线。这样我的笔记本也能有打印口,从而支持GBA游戏烧录。

然后看到有这个有爱的帖子,《火线烧录的相关科普知识及软件下载》
http://forum.tgbus.com/viewthread.php?tid=2365432&extra=page%3D2

在里面我找到了坑爹的火线烧录程序破解版,省去了我自己搞掂它的时间限制的麻烦。于是我有了:
1、火线3.0烧录程序破解版。
2、LPT(打印口)的火线一根。
3、GBA烧录卡及原装GBASP一台。
4、蛋疼的PCI-Express转LPT(打印口)转接线一根。

接下来,win7操作系统不用想了,64位32位都没有机会,因为winio.dll是xp的版本。所以想要烧录必须增加这个条件:
5、XP操作系统。

有了这些还不够,坑爹的LPT地址问题出现了。因为这种接口以前是主板上带着的,一般都是固定地址的,所以地址就被硬编码到火线的程序中去了。打开火线,切到设置界面,有个”默认LPT端口”什么的选项,勾掉之后就可以在下拉菜单中选择:

看图,火线的默认端口地址就是LPT1(378)。

我们看到地址有很多,LPT1(0x378),LPT2(xxx)什么的。但基本不用想直接用,因为只要不是主板上自带的LPT,而是用转接线,地址一定是大于0x1000的。虽然火线开发者后面有做PCI2LPT1/PCI2LPT2,貌似是给转接卡用的,但是这里的地址被硬编码到0x9000和0x9400,不一定和手头的转接线地址相符。因此我们还需要做2件事情:
a) 搞清楚手头的LPT端口地址是神马?
b) 把这个地址写到火线烧录程序里面去。

==========================================
如何搞清楚当前转接线的LPT的地址呢?答案是打开(控制面板==>系统==>设备管理器,你懂的,就是那个设备管理器嘛)
然后看(端口==>LPT1,或者你自己买的神马转接线,总之应该在端口下面有叫做LPT的端口)
双击看它的属性,切换到资源页,看I/O范围的值,很可能有两项,不用管,只看第一项,它是AAAA – BBBB的形式。
好吧,说了这么多,LPT的地址就是这个AAAA。


看到图的话,我这里是4CE8,问题(a)解决:LPT的地址到手,4CE8
==========================================

然后我们来解决第二个问题,那就是如何把这个值弄到火线的烧录程序里。这就需要修改程序代码了,因为火线烧录程序木有提供输入LPT地址的地方。不得不说那个火线破解版很赞,去掉了火线坑爹的时间限制代码,但它居然不厚道的加了壳。所以如果你决定手动修改火线破解版(= =b),先要找个脱壳的东西,比如FileScanner之类的就可以。在命令行执行:fs -u gabfire.exe,脱掉蛋疼的壳,搞出一个比原来exe大一些的exe,只有这样程序才可以直接修改,否则你搜不到我说的那一串数。

接下来就好办了,我直接上结论,假如我们修改火线默认选项中LPT1(378) 的地址,方法如下:
1、需要有WinHex这个软件,或者你熟悉其他16进制编辑器也可以,我们要用到搜索==>查找16进制数值(ctrl+alt+f)功能。

2、对于脱壳的3.0版火线,搜索C7812403000078030000这一串16进制数,我保证应该只能搜出一个位置。
对于脱壳的2.6版火线,搜索C7811C03000078030000这一串16进制数,也应该只有一个位置有。

3、我标黑的那个地方就是程序内设的LPT1的地址,玩过游戏内存数据修改的就会明白,这就是LPT1的默认地址378,被反转高低位写在内存里。

4、对于我的LPT转接线,地址应该是4CE8,反过来写就是E84C,所以只要把7803改成E84C即可完成任务。
注意!E84C是我手头的转接线给出的地址,不同牌子的转接线地址是不一样的,甚至重启之后有的转接线地址也会变。

保存之后,我们得到了一个符合自己的打印口地址的火线定制客户端。
==========================================

好吧,万事俱备!运行火线,gbasp开机select+start按住,进入gba的扩展模式,再点火线识别卡带(这些操作火线说明书上有写)!

叮叮叮是不是识别出来了?恭喜!!经过一系列复杂的折腾,我们终于可以在没有LPT(打印口)的笔记本上使用火线烧录了!

嗯嗯……或许你一次不能成功= =b,那就需要注意下是不是选了火线中的LPT1?地址是不是修改对了?你的转接线究竟好不好使?不要买街上那种20块的usb转lpt线,那种不行的。要买PCMIC转LPT或者PCI-Express转LPT的线……

神马?这种线很贵?比usb的烧录卡还贵?嗯嗯……你知道的,有时候人就是这么蛋疼= =b

(本帖完)

==========================================
PS1:
最后吐个槽,我手头的神州卡II 256m的那种,火线识别不能。但是另外一款烧录软件Magic Flash 2.0可以识别。还有,火线的水蓝卡恢复存档这个功能一直不好使,我不知道是不是存档格式的问题。备份下来都是64k,切割一下可以给VBA用。但是无论如何都没办法恢复到卡上。以后再研究吧,不知道是不是我的水蓝卡没电了= =b。

PS2:
如果有人想修改Magic Flash的默认LPT地址,稍微麻烦一些。脱壳要用Armadillounpack,得到脱壳的版本后,搜索C705685A4F004001,应该可以搜到2个位置符合。里面的4001是第四个端口LPT4(140)的地址,把两处的4001统统改成需要的地址,比如像我那种改成E84C就可以了。注意这样修改之后,要在设置里选LPT4哦。不改LPT1是因为LPT1的地址有4个,改起来费劲。