繼續(xù)未完成的內(nèi)容,聲明
本文僅用于學(xué)習(xí)研究,不提供解壓工具和實(shí)際代碼。
由于時(shí)間倉(cāng)促,劍3資源格式分析(僅用于學(xué)習(xí)和技術(shù)研究)(一)
大部分只是貼出了分析的結(jié)果,并沒(méi)有詳細(xì)的分析過(guò)程,比如:如何知道那是一個(gè)pak文件處理對(duì)象,
如何根據(jù)虛表偏移獲取實(shí)際函數(shù)地址等等,這就需要讀者對(duì)c++對(duì)象在內(nèi)存中的layout有一定基礎(chǔ)。
開(kāi)始正文了~~,先整理下前面的分析結(jié)果:
1、劍3是通過(guò)package.ini 來(lái)管理pak文件的,最多可配置key從 0-32(0x20)的32個(gè)文件。
2、每個(gè)pak文件都用一個(gè)獨(dú)立對(duì)象來(lái)管理,所有的pak對(duì)象指針存儲(chǔ)在一個(gè)數(shù)組里(這個(gè)后面會(huì)用到)。
3、pak文件格式:[pak標(biāo)記(Uint32)] + [文件數(shù)目(Uint32)]+[索引數(shù)據(jù)偏移(Uint32)]+未知內(nèi)容。另外,每個(gè)文件的索引數(shù)據(jù)是16個(gè)字節(jié)。
一、路徑名哈希
劍3的pak的內(nèi)部文件是通過(guò)hash值來(lái)查找的,這樣有利于加快查詢(xún)速度。這就需要有一個(gè)函數(shù)通過(guò)傳入 路徑名返回hash值。
這個(gè)函數(shù)居然是導(dǎo)出的。。。g_FileNameHash
這個(gè)函數(shù)代碼比較少,可以逆向出來(lái)用C重寫(xiě),也可以直接使用引擎函數(shù)(LoadLibrary,GetProceAddress來(lái)使用)。
熟悉劍俠系列的朋友會(huì)發(fā)現(xiàn),這個(gè)函數(shù)從新劍俠情緣(可能歷史更久)開(kāi)始就沒(méi)有變過(guò)(確實(shí)沒(méi)必要變),具體細(xì)節(jié)就不逐個(gè)分析了,我是寫(xiě)了一個(gè)單獨(dú)的命令行工具
來(lái)測(cè)試的。
二、查詢(xún)過(guò)程
查詢(xún)函數(shù)在sub_10010E00
里,也就是(0x10010E00)的位置,我是通過(guò)簡(jiǎn)單分析g_IsFileExist 得知這個(gè)函數(shù)功能的。下面
來(lái)分析這個(gè)函數(shù)過(guò)程:

從前文可知,pak文件對(duì)象是存儲(chǔ)在一個(gè)數(shù)組的這個(gè)數(shù)組是類(lèi)似 KPakFile* m_szPakFile[0x21];
前面0x20個(gè)存儲(chǔ)的都是KPakFile對(duì)象指針,最后一個(gè)存儲(chǔ)的是數(shù)組長(zhǎng)度。
這個(gè)搜索結(jié)構(gòu)比較簡(jiǎn)單,就是遍歷所有的KPakFile對(duì)象,逐個(gè)查詢(xún),找到了就返回。想知道具體怎么查詢(xún)的嗎,
接下來(lái)要看sub_100108B0了。
這個(gè)函數(shù)稍微有點(diǎn)長(zhǎng),分幾個(gè)部分來(lái)分析吧:

首先,驗(yàn)證下Hash值是否是0,如果是0,肯定是錯(cuò)了:)
然后接著開(kāi)始根據(jù)這個(gè)hash值進(jìn)行查找了,經(jīng)過(guò)分析,我發(fā)現(xiàn)這個(gè)函數(shù)其實(shí)是一個(gè)二分查找,代碼貼出來(lái)如下 sub_10010320
:


從上面的代碼還是比較容易可以知道,每個(gè)文件的16個(gè)索引數(shù)據(jù)中,前4個(gè)字節(jié)是hash值,這個(gè)函數(shù)返回的是這個(gè)文件是pak包的第幾個(gè)。
接著前面的sub_100108B0
來(lái)看吧

這一段是保存查詢(xún)到的數(shù)據(jù)到對(duì)象里。分析到這里,我只知道16個(gè)索引數(shù)據(jù)前4個(gè)字節(jié)是hash值,那么剩下的12個(gè)字節(jié)呢,
剩下的數(shù)據(jù)基本可以確定是:文件偏移、文件長(zhǎng)度。我是個(gè)懶人,接下來(lái)的分析我是通過(guò)在fseek、fread下斷點(diǎn)來(lái)得到的,為什么不是在SetFilePointer和ReadFile呢,
這是根據(jù)前面的分析得到的,因?yàn)閜ak文件管理對(duì)象使用的是C標(biāo)準(zhǔn)庫(kù)函數(shù)。
根據(jù)fread和fseek的結(jié)果,可以得到如下結(jié)果:
索引數(shù)據(jù)構(gòu)成是:
[哈希數(shù)值(Uint32)] + [文件偏移(Uint32)]+[未知數(shù)據(jù)(Uint32)] + 2(文件長(zhǎng)度)+2(未知數(shù)據(jù))。
剩下的,就是看看單獨(dú)內(nèi)部文件的解壓方式了,
在fread的緩沖區(qū)上設(shè)置內(nèi)存斷點(diǎn),就可以找到解壓函數(shù)了:
sub_10018020
這個(gè)函數(shù)不算太長(zhǎng),一開(kāi)始我也想逆向成C語(yǔ)言,后來(lái)看到如此多的分支就放棄了,轉(zhuǎn)而用了一個(gè)偷懶的辦法解決了:
從匯編代碼可知這個(gè)函數(shù)的原型:
typedef int (*PUNPACK_FUN)(void* psrcData, int nSrcLen, void* pDstData, int* pDstLen);
直接加載劍3的dll,設(shè)置函數(shù)地址:
PUNPACK_FUN pEngineUnpack = (PUNPACK_FUN)((unsigned int)hEngineModule + 0x18020);
hEngineModule
是引擎dll的基址,大家看到了吧,dll的函數(shù)即使不導(dǎo)出,我們也是可以調(diào)用的:)
三、尾聲
到這里,已經(jīng)可以寫(xiě)出一個(gè)pak文件的解壓包了,但是,我們還是沒(méi)有還原真實(shí)的文件名,
下面是我解壓的script.pak的文件的部分內(nèi)容:

終于看到大俠們的簽名了。當(dāng)然,對(duì)著一堆hash值為名字的文件,閱讀起來(lái)確實(shí)很困難,
那么有辦法還原真實(shí)的文件名嗎,辦法還是有一些的,可以通過(guò)各種辦法改寫(xiě)g_OpenFileInPak記錄參數(shù)名,來(lái)獲取游戲中用到的pak內(nèi)部文件名,相信這難不倒各位了。
posted @
2010-07-16 20:47 feixuwu 閱讀(4760) |
評(píng)論 (11) |
編輯 收藏
這幾天在玩劍三,突然興趣來(lái)了,想要分析劍3的資源打包格式。在資源分析和逆向方面原來(lái)偶爾也干過(guò),
不過(guò)總體來(lái)說(shuō)還是處于菜鳥(niǎo)階段,這篇文章希望和其他有興趣的兄弟分享下這幾天的經(jīng)歷,僅僅作為技術(shù)研究。
一、安全保護(hù)
一般來(lái)說(shuō),很少有游戲的資源格式可以直接通過(guò)分析資源文件本身得到答案,大部分難免要靜態(tài)逆向、動(dòng)態(tài)調(diào)試。
無(wú)論是靜態(tài)逆向還是動(dòng)態(tài)調(diào)試,首先需要知道當(dāng)前exe和dll的保護(hù)情況,用peid查看,發(fā)現(xiàn)只有g(shù)ameupdater.exe 用upx加殼了。不太明白金山為什么對(duì)客戶(hù)端沒(méi)有加殼。
其實(shí)我并不關(guān)心gameupdater.exe 是否加殼,畢竟要?jiǎng)討B(tài)分析的目標(biāo)是JX3Client.exe
,要?jiǎng)討B(tài)調(diào)試JX3Client.exe,首先要解決啟動(dòng)參數(shù)問(wèn)題。
二、啟動(dòng)參數(shù)
如果直接啟動(dòng)JX3Client.exe,JX3Client.exe會(huì)直接退出,并啟動(dòng)gameuodater.exe,然后通過(guò)gameupdater.exe啟動(dòng)JX3Client.exe。
這種啟動(dòng)方式會(huì)影響動(dòng)態(tài)調(diào)試,所以首先我需要找出JX3Client.exe的啟動(dòng)參數(shù)。打開(kāi)IDA逆向,轉(zhuǎn)到啟動(dòng)處,匯編代碼如下:

start proc near
call ___security_init_cookie
jmp ___tmainCRTStartup
start endp
這是一個(gè)典型的VC程序入口,在___tmainCRTStartup
里,crt會(huì)初始化全局變量、靜態(tài)變量,然后進(jìn)入main,我們需要做的是直接找到main,
跟進(jìn)去,會(huì)發(fā)現(xiàn)IDA已經(jīng)幫我們找到WinMain了,直接跟進(jìn)去,
關(guān)鍵代碼在WinMain的入口處:

從這個(gè)代碼片段可以知道,WinMain開(kāi)始就比較了命令行參數(shù)是否是"DOTNOTSTARTGAMEBYJX3CLIENT.EXE
",如果不是,
則轉(zhuǎn)到啟動(dòng)更新程序了。這個(gè)好辦,我們寫(xiě)一個(gè)run.bat,內(nèi)容只有一行:
JX3Client.exe DOTNOTSTARTGAMEBYJX3CLIENT.EXE
運(yùn)行,果然,直接看到加載界面了。
三、PAK文件管理
在劍3里,PAK目錄下有很多PAK文件,劍3是通過(guò)package.ini 來(lái)加載和管理pak內(nèi)部文件的。
這個(gè)文件內(nèi)容如下:
[SO3Client]
10=data_5.pak
1=ui.pak
0=update_1.pak
3=maps.pak
2=settings.pak
5=scripts.pak
4=represent.pak
7=data_2.pak
6=data_1.pak
9=data_4.pak
Path=.\pak
8=data_3.pak
基本上PAK目錄下所有的PAK文件都列出來(lái)了,其實(shí)劍3的資源文件打包方式基本上和新劍俠情緣類(lèi)似(細(xì)節(jié)還是有比較大的差別)。
打開(kāi)ollyDbg,帶參數(shù)啟動(dòng)JX3Client.exe,在CreateFile設(shè)置斷點(diǎn),可以發(fā)現(xiàn),package.ini
的讀取和處理是在
Engine_Lua5.dll
的g_LoadPackageFiles
函數(shù),熟悉新劍俠情緣資源管理方式的同學(xué)大概會(huì)猜到這個(gè)函數(shù)是做什么的,先看看函數(shù)內(nèi)容吧,這個(gè)函數(shù)比較長(zhǎng)
只能逐步的分析了,首先是打開(kāi)ini文件

使用g_OpenIniFile打開(kāi)前面提到的ini文件,如果打開(kāi)失敗,自然直接返回了。
打開(kāi)成功后,循環(huán)讀取ini配置的文件,讀取的section是SO3Client 讀取的key是0到0x20。
loc_1001119A: ; int
push 0Ah
lea ecx, [esp+1A0h+var_178]
push ecx ; char *
push ebx ; int
call ds:_itoa ; 這是根據(jù)數(shù)字生成key的代碼
mov edx, [ebp+0]
mov edx, [edx+24h]
add esp, 0Ch
push 40h
lea eax, [esp+1A0h+var_168]
push eax
mov eax, [esp+1A4h+var_184]
push offset unk_10035B8C
lea ecx, [esp+1A8h+var_178]
push ecx
push eax
mov ecx, ebp
call edx ; 讀取INI內(nèi)容 readString(section, key)
test eax, eax
jz loc_1001127A
這段是通過(guò)readString("SO3Client", key)來(lái)獲取pak文件名, key就是"0"~"32"的字符串,也就是最多能配置32個(gè)Pak文件。
獲得了pak文件名后,下面就是打開(kāi)和保存pak文件的索引數(shù)據(jù)了。

后面的注釋是我分析的時(shí)候加上的,IDA這個(gè)功能不錯(cuò)!
首先new一個(gè)0x20字節(jié)的空間用來(lái)存儲(chǔ)pak對(duì)象(我自己命名的類(lèi)),接著調(diào)用構(gòu)造函數(shù),創(chuàng)建pak對(duì)象。
創(chuàng)建對(duì)象后,要用這個(gè)Pak對(duì)象打開(kāi)對(duì)應(yīng)的pak文件了,這是我們下面的代碼:

首先通過(guò)
mov [edi+edx*4], eax
將對(duì)象保存,然后,調(diào)用這個(gè)類(lèi)的成員函數(shù)打開(kāi)pak文件,具體代碼在sub_10010ca0。

這段代碼的意思很明白了,打開(kāi)文件,讀取0x20的文件頭,

這里做的是驗(yàn)證文件格式,和一些必要的驗(yàn)證。

這段是讀取pak內(nèi)部文件數(shù)目,讀取索引數(shù)據(jù),以備后面查詢(xún)使用。
到此為止,所有pak文件的管理對(duì)象都已經(jīng)加載和設(shè)置完畢了。
以上內(nèi)容看起來(lái)很順理成章,但是實(shí)際上凝聚了無(wú)數(shù)的失敗和重試。
后面是pak內(nèi)部文件的查找和讀取了。
剩下的內(nèi)容明天貼了~~~
posted @
2010-07-15 21:07 feixuwu 閱讀(5222) |
評(píng)論 (10) |
編輯 收藏
最近有朋友在面試的時(shí)候被問(wèn)了select 和epoll效率差的原因,和一般人一樣,大部分都會(huì)回答select是輪詢(xún)、epoll是觸發(fā)式的,所以效率高。這個(gè)答案聽(tīng)上去很完美,大致也說(shuō)出了二者的主要區(qū)別。
今天閑來(lái)無(wú)事,翻看了下內(nèi)核代碼,結(jié)合內(nèi)核代碼和大家分享下我的觀點(diǎn)。
一、連接數(shù)
我本人也曾經(jīng)在項(xiàng)目中用過(guò)select和epoll,對(duì)于select,感觸最深的是linux下select最大數(shù)目限制(windows 下似乎沒(méi)有限制),每個(gè)進(jìn)程的select最多能處理FD_SETSIZE個(gè)FD(文件句柄),
如果要處理超過(guò)1024個(gè)句柄,只能采用多進(jìn)程了。
常見(jiàn)的使用slect的多進(jìn)程模型是這樣的: 一個(gè)進(jìn)程專(zhuān)門(mén)accept,成功后將fd通過(guò)unix socket傳遞給子進(jìn)程處理,父進(jìn)程可以根據(jù)子進(jìn)程負(fù)載分派。曾經(jīng)用過(guò)1個(gè)父進(jìn)程+4個(gè)子進(jìn)程 承載了超過(guò)4000個(gè)的負(fù)載。
這種模型在我們當(dāng)時(shí)的業(yè)務(wù)運(yùn)行的非常好。epoll在連接數(shù)方面沒(méi)有限制,當(dāng)然可能需要用戶(hù)調(diào)用API重現(xiàn)設(shè)置進(jìn)程的資源限制。
二、IO差別
1、select的實(shí)現(xiàn)
這段可以結(jié)合linux內(nèi)核代碼描述了,我使用的是2.6.28,其他2.6的代碼應(yīng)該差不多吧。
先看看select:
select系統(tǒng)調(diào)用的代碼在fs/Select.c下,
asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp,
fd_set __user *exp, struct timeval __user *tvp)
{
struct timespec end_time, *to = NULL;
struct timeval tv;
int ret;
if (tvp) {
if (copy_from_user(&tv, tvp, sizeof(tv)))
return -EFAULT;
to = &end_time;
if (poll_select_set_timeout(to,
tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
(tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
return -EINVAL;
}
ret = core_sys_select(n, inp, outp, exp, to);
ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
return ret;
}
前面是從用戶(hù)控件拷貝各個(gè)fd_set到內(nèi)核空間,接下來(lái)的具體工作在core_sys_select中,
core_sys_select->do_select,真正的核心內(nèi)容在do_select里:
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
ktime_t expire, *to = NULL;
struct poll_wqueues table;
poll_table *wait;
int retval, i, timed_out = 0;
unsigned long slack = 0;
rcu_read_lock();
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0)
return retval;
n = retval;
poll_initwait(&table);
wait = &table.pt;
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait = NULL;
timed_out = 1;
}
if (end_time && !timed_out)
slack = estimate_accuracy(end_time);
retval = 0;
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
set_current_state(TASK_INTERRUPTIBLE);
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
const struct file_operations *f_op = NULL;
struct file *file = NULL;
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) {
i += __NFDBITS;
continue;
}
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
int fput_needed;
if (i >= n)
break;
if (!(bit & all_bits))
continue;
file = fget_light(i, &fput_needed);
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll)
mask = (*f_op->poll)(file, retval ? NULL : wait);
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
retval++;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
}
}
}
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
cond_resched();
}
wait = NULL;
if (retval || timed_out || signal_pending(current))
break;
if (table.error) {
retval = table.error;
break;
}
/*
* If this is the first loop and we have a timeout
* given, then we convert to ktime_t and set the to
* pointer to the expiry value.
*/
if (end_time && !to) {
expire = timespec_to_ktime(*end_time);
to = &expire;
}
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
timed_out = 1;
}
__set_current_state(TASK_RUNNING);
poll_freewait(&table);
return retval;
}
上面的代碼很多,其實(shí)真正關(guān)鍵的代碼是這一句:
mask = (*f_op->poll)(file, retval ? NULL : wait);
這個(gè)是調(diào)用文件系統(tǒng)的 poll函數(shù),不同的文件系統(tǒng)poll函數(shù)自然不同,由于我們這里關(guān)注的是tcp連接,而socketfs的注冊(cè)在 net/Socket.c里。
register_filesystem(&sock_fs_type);
socket文件系統(tǒng)的函數(shù)也是在net/Socket.c里:
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.aio_read = sock_aio_read,
.aio_write = sock_aio_write,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.open = sock_no_open, /* special open code to disallow open via /proc */
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
從sock_poll跟隨下去,
最后可以到 net/ipv4/tcp.c的
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
這個(gè)是最終的查詢(xún)函數(shù),
也就是說(shuō)select 的核心功能是調(diào)用tcp文件系統(tǒng)的poll函數(shù),不停的查詢(xún),如果沒(méi)有想要的數(shù)據(jù),主動(dòng)執(zhí)行一次調(diào)度(防止一直占用cpu),直到有一個(gè)連接有想要的消息為止。
從這里可以看出select的執(zhí)行方式基本就是不同的調(diào)用poll,直到有需要的消息為止,如果select 處理的socket很多,這其實(shí)對(duì)整個(gè)機(jī)器的性能也是一個(gè)消耗。
2、epoll的實(shí)現(xiàn)
epoll的實(shí)現(xiàn)代碼在 fs/EventPoll.c下,
由于epoll涉及到幾個(gè)系統(tǒng)調(diào)用,這里不逐個(gè)分析了,僅僅分析幾個(gè)關(guān)鍵點(diǎn),
第一個(gè)關(guān)鍵點(diǎn)在
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
struct file *tfile, int fd)
這是在我們調(diào)用sys_epoll_ctl 添加一個(gè)被管理socket的時(shí)候調(diào)用的函數(shù),關(guān)鍵的幾行如下:
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
/*
* Attach the item to the poll hooks and get current event bits.
* We can safely use the file* here because its usage count has
* been increased by the caller of this function. Note that after
* this operation completes, the poll callback can start hitting
* the new item.
*/
revents = tfile->f_op->poll(tfile, &epq.pt);
這里也是調(diào)用文件系統(tǒng)的poll函數(shù),不過(guò)這次初始化了一個(gè)結(jié)構(gòu),這個(gè)結(jié)構(gòu)會(huì)帶有一個(gè)poll函數(shù)的callback函數(shù):ep_ptable_queue_proc,
在調(diào)用poll函數(shù)的時(shí)候,會(huì)執(zhí)行這個(gè)callback,這個(gè)callback的功能就是將當(dāng)前進(jìn)程添加到 socket的等待進(jìn)程上。
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
poll_table *pt)
{
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;
if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead;
pwq->base = epi;
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
} else {
/* We have to signal that an error occurred */
epi->nwait = -1;
}
}
注意到參數(shù) whead
實(shí)際上是 sk->sleep,其實(shí)就是將當(dāng)前進(jìn)程添加到sk的等待隊(duì)列里,當(dāng)該socket收到數(shù)據(jù)或者其他事件觸發(fā)時(shí),會(huì)調(diào)用
sock_def_readable
或者sock_def_write_space
通知函數(shù)來(lái)喚醒等待進(jìn)程,這2個(gè)函數(shù)都是在socket創(chuàng)建的時(shí)候填充在sk結(jié)構(gòu)里的。
從前面的分析來(lái)看,epoll確實(shí)是比select聰明的多、輕松的多,不用再苦哈哈的去輪詢(xún)了。
posted @
2010-07-10 18:40 feixuwu 閱讀(10257) |
評(píng)論 (3) |
編輯 收藏
昨天一個(gè)同事一大早在群里推薦了一個(gè)google project上的開(kāi)源內(nèi)存分配器(
http://code.google.com/p/google-perftools/),據(jù)說(shuō)google的很多產(chǎn)品都用到了這個(gè)內(nèi)存分配庫(kù),而且經(jīng)他測(cè)試,我們的游戲客戶(hù)端集成了這個(gè)最新內(nèi)存分配器后,F(xiàn)PS足足提高了將近10幀左右,這可是個(gè)了不起的提升,要知道3D組的兄弟忙了幾周也沒(méi)見(jiàn)這么大的性能提升。
如果我們自己本身用的crt提供的內(nèi)存分配器,這個(gè)提升也算不得什么。問(wèn)題是我們內(nèi)部系統(tǒng)是有一個(gè)小內(nèi)存管理器的,一般來(lái)說(shuō)小內(nèi)存分配的算法都大同小異,現(xiàn)成的實(shí)現(xiàn)也很多,比如linux內(nèi)核的slab、SGI STL的分配器、ogre自帶的內(nèi)存分配器,我們自己的內(nèi)存分配器也和前面列舉的實(shí)現(xiàn)差不多。讓我們來(lái)看看這個(gè)項(xiàng)目有什么特別的吧。
一、使用方法
打開(kāi)主頁(yè),由于公司網(wǎng)絡(luò)禁止SVN從外部更新,所以只能下載了打包的源代碼。解壓后,看到有個(gè)doc目錄,進(jìn)去,打開(kāi)使用文檔,發(fā)現(xiàn)使用方法極為簡(jiǎn)單:
To use TCMalloc, just link TCMalloc into your application via the
"-ltcmalloc" linker flag.再看算法,也沒(méi)什么特別的,還是和slab以及SGI STL分配器類(lèi)似的算法。
unix環(huán)境居然只要鏈接這個(gè)tcmalloc庫(kù)就可以了!,太方便了,不過(guò)我手頭沒(méi)有l(wèi)inux環(huán)境,文檔上也沒(méi)提到windows環(huán)境怎么使用,
打開(kāi)源代碼包,有個(gè)vs2003解決方案,打開(kāi),隨便挑選一個(gè)測(cè)試項(xiàng)目,查看項(xiàng)目屬性,發(fā)現(xiàn)僅僅有2點(diǎn)不同:
1、鏈接器命令行里多了
"..\..\release\libtcmalloc_minimal.lib",就是鏈接的時(shí)候依賴(lài)了這個(gè)內(nèi)存優(yōu)化庫(kù)。
2、鏈接器->輸入->強(qiáng)制符號(hào)引用 多了 __tcmalloc。
這樣就可以正確的使用tcmalloc庫(kù)了,測(cè)試了下,測(cè)試項(xiàng)目運(yùn)行OK!
二、如何替換CRT的malloc
從前面的描述可知,項(xiàng)目強(qiáng)制引用了__tcmalloc, 搜索了測(cè)試代碼,沒(méi)發(fā)現(xiàn)用到_tcmalloc相關(guān)的函數(shù)和變量,這個(gè)選項(xiàng)應(yīng)該是為了防止dll被優(yōu)化掉(因?yàn)榇a里沒(méi)有什么地方用到這個(gè)dll的符號(hào))。
初看起來(lái),鏈接這個(gè)庫(kù)后,不會(huì)影響任何現(xiàn)有代碼:我們沒(méi)有引用這個(gè)Lib庫(kù)的頭文件,也沒(méi)有使用過(guò)這個(gè)dll的導(dǎo)出函數(shù)。那么這個(gè)dll是怎么優(yōu)化應(yīng)用程序性能的呢?
實(shí)際調(diào)試,果然發(fā)現(xiàn)問(wèn)題了,看看如下代碼
void* pData = malloc(100);
00401085 6A 64 push 64h
00401087 FF 15 A4 20 40 00 call dword ptr [__imp__malloc (4020A4h)]
跟蹤 call malloc這句,step進(jìn)去,發(fā)現(xiàn)是
78134D09 E9 D2 37 ED 97 jmp `anonymous namespace'::LibcInfoWithPatchFunctions<8>::Perftools_malloc (100084E0h)
果然,從這里開(kāi)始,就跳轉(zhuǎn)到libtcmalloc提供的Perftools_malloc了。
原來(lái)是通過(guò)API掛鉤來(lái)實(shí)現(xiàn)無(wú)縫替換系統(tǒng)自帶的malloc等crt函數(shù)的,而且還是通過(guò)大家公認(rèn)的不推薦的改寫(xiě)函數(shù)入口指令來(lái)實(shí)現(xiàn)的,一般只有在游戲外掛和金山詞霸之類(lèi)的軟件才會(huì)用到這樣的掛鉤技術(shù),
而且金山詞霸經(jīng)常需要更新補(bǔ)丁解決不同系統(tǒng)兼容問(wèn)題。
三、性能差別原因
如前面所述,tcmalloc確實(shí)用了很hacker的辦法來(lái)實(shí)現(xiàn)無(wú)縫的替換系統(tǒng)自帶的內(nèi)存分配函數(shù)(本人在使用這類(lèi)技術(shù)通常是用來(lái)干壞事的。。。),但是這也不足以解釋為什么它的效率比我們自己的好那么多。
回到tcmalloc 的手冊(cè),tcmalloc除了使用常規(guī)的小內(nèi)存管理外,對(duì)多線程環(huán)境做了特殊處理,這和我原來(lái)見(jiàn)到的內(nèi)存分配器大有不同,一般的內(nèi)存分配器作者都會(huì)偷懶,把多線程問(wèn)題扔給使用者,大多是加
個(gè)bool型的模板參數(shù)來(lái)表示是否是多線程環(huán)境,還美其名曰:可定制,末了還得吹噓下模板的優(yōu)越性。
tcmalloc是怎么做的呢? 答案是每線程一個(gè)ThreadCache,大部分操作系統(tǒng)都會(huì)支持thread local storage 就是傳說(shuō)中的TLS,這樣就可以實(shí)現(xiàn)每線程一個(gè)分配器了,
這樣,不同線程分配都是在各自的threadCache里分配的。我們的項(xiàng)目的分配器由于是多線程環(huán)境的,所以不管三七二十一,全都加鎖了,性能自然就低了。
僅僅是如此,還是不足以將tcmalloc和ptmalloc2分個(gè)高下,后者也是每個(gè)線程都有threadCache的。
關(guān)于這個(gè)問(wèn)題,doc里有一段說(shuō)明,原文貼出來(lái):
ptmalloc2 also reduces lock contention by using per-thread arenas but
there is a big problem with ptmalloc2's use of per-thread arenas. In
ptmalloc2 memory can never move from one arena to another. This can
lead to huge amounts of wasted space.
大意是這樣的:ptmalloc2 也是通過(guò)tls來(lái)降低線程鎖,但是ptmalloc2各個(gè)線程的內(nèi)存是獨(dú)立的,也就是說(shuō),第一個(gè)線程申請(qǐng)的內(nèi)存,釋放的時(shí)候還是必須放到第一個(gè)線程池中(不可移動(dòng)),這樣可能導(dǎo)致大量?jī)?nèi)存浪費(fèi)。
四、代碼細(xì)節(jié)
1、無(wú)縫替換malloc等crt和系統(tǒng)分配函數(shù)。
前面提到tcmalloc會(huì)無(wú)縫的替換掉原有dll中的malloc,這就意味著使用tcmalloc的項(xiàng)目必須是 MD(多線程dll)或者M(jìn)Dd(多線程dll調(diào)試)。tcmalloc的dll定義了一個(gè)
static TCMallocGuard module_enter_exit_hook;
的靜態(tài)變量,這個(gè)變量會(huì)在dll加載的時(shí)候先于DllMain運(yùn)行,在這個(gè)類(lèi)的構(gòu)造函數(shù),會(huì)運(yùn)行PatchWindowsFunctions來(lái)掛鉤所有dll的 malloc、free、new等分配函數(shù),這樣就達(dá)到了替換功能,除此之外,
為了保證系統(tǒng)兼容性,掛鉤API的時(shí)候還實(shí)現(xiàn)了智能分析指令,否則寫(xiě)入第一條Jmp指令的時(shí)候可能會(huì)破環(huán)后續(xù)指令的完整性。
2、LibcInfoWithPatchFunctions 和ThreadCache。
LibcInfoWithPatchFunctions模板類(lèi)包含tcmalloc實(shí)現(xiàn)的優(yōu)化后的malloc等一系列函數(shù)。LibcInfoWithPatchFunctions的模板參數(shù)在我看來(lái)沒(méi)什么用處,tcmalloc默認(rèn)可以掛鉤
最多10個(gè)帶有malloc導(dǎo)出函數(shù)的庫(kù)(我想肯定是夠用了)。ThreadCache在每個(gè)線程都會(huì)有一個(gè)TLS對(duì)象:
__thread ThreadCache* ThreadCache::threadlocal_heap_。
3、可能的問(wèn)題
設(shè)想下這樣一個(gè)情景:假如有一個(gè)dll 在tcmalloc之前加載,并且在分配了內(nèi)存(使用crt提供的malloc),那么在加載tcmalloc后,tcmalloc會(huì)替換所有的free函數(shù),然后,在某個(gè)時(shí)刻,
在前面的那個(gè)dll代碼中釋放該內(nèi)存,這豈不是很危險(xiǎn)。實(shí)際測(cè)試發(fā)現(xiàn)沒(méi)有任何問(wèn)題,關(guān)鍵在這里:
span = Static::pageheap()->GetDescriptor(p);
if (!span) {
// span can be NULL because the pointer passed in is invalid
// (not something returned by malloc or friends), or because the
// pointer was allocated with some other allocator besides
// tcmalloc. The latter can happen if tcmalloc is linked in via
// a dynamic library, but is not listed last on the link line.
// In that case, libraries after it on the link line will
// allocate with libc malloc, but free with tcmalloc's free.
(*invalid_free_fn)(ptr); // Decide how to handle the bad free request
return;
}
tcmalloc會(huì)通過(guò)span識(shí)別這個(gè)內(nèi)存是否自己分配的,如果不是,tcmalloc會(huì)調(diào)用該dll原始對(duì)應(yīng)函數(shù)(這個(gè)很重要)釋放。這樣就解決了這個(gè)棘手的問(wèn)題。
五、其他
其實(shí)tcmalloc使用的每個(gè)技術(shù)點(diǎn)我從前都用過(guò),但是我從來(lái)沒(méi)想過(guò)用API掛鉤來(lái)實(shí)現(xiàn)這樣一個(gè)有趣的內(nèi)存優(yōu)化庫(kù)(即使想過(guò),也是一閃而過(guò)就否定了)。
從tcmalloc得到靈感,結(jié)合常用的外掛技術(shù),可以很輕松的開(kāi)發(fā)一個(gè)獨(dú)立工具:這個(gè)工具可以掛載到指定進(jìn)程進(jìn)行內(nèi)存優(yōu)化,在我看來(lái),這可能可以作為一個(gè)外掛輔助工具來(lái)優(yōu)化那些
內(nèi)存優(yōu)化做的很差導(dǎo)致幀速很低的國(guó)產(chǎn)游戲。
posted @
2010-07-10 17:32 feixuwu 閱讀(10073) |
評(píng)論 (14) |
編輯 收藏