• <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>
            posts - 14,  comments - 57,  trackbacks - 0
             
               繼續(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)編輯 收藏
            僅列出標(biāo)題
            共2頁(yè): 1 2 
            <2011年5月>
            24252627282930
            1234567
            891011121314
            15161718192021
            22232425262728
            2930311234

            文章轉(zhuǎn)載請(qǐng)注明出處

            常用鏈接

            留言簿(11)

            隨筆分類(lèi)

            隨筆檔案

            搜索

            •  

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            久久超碰97人人做人人爱| 热久久这里只有精品| 国产成人精品三上悠亚久久| 久久久久亚洲精品天堂| 2021少妇久久久久久久久久| 日本久久中文字幕| 麻豆精品久久精品色综合| 久久久久亚洲AV成人网人人网站 | 亚洲午夜久久久精品影院| 亚洲精品国精品久久99热一| 日日狠狠久久偷偷色综合96蜜桃| 亚洲国产精品久久久天堂| 久久r热这里有精品视频| 亚洲成色WWW久久网站| 国产国产成人久久精品| 亚洲国产精品久久| 狠狠色狠狠色综合久久| 国产精品永久久久久久久久久| 久久伊人精品一区二区三区| 亚洲午夜无码AV毛片久久| 青青草原综合久久大伊人导航| 久久精品国产亚洲AV无码娇色| 久久久久亚洲AV成人网人人网站| 国产精品久久久久…| 色婷婷综合久久久久中文| 成人综合伊人五月婷久久| 久久国产热精品波多野结衣AV| 久久久综合香蕉尹人综合网| 日本精品久久久久中文字幕| 亚洲AV无码一区东京热久久| 亚洲午夜无码AV毛片久久| 久久免费大片| 久久性生大片免费观看性| 久久99久久成人免费播放| 99久久精品免费看国产一区二区三区| 精品久久久无码中文字幕| 久久精品国产99国产电影网| 国产成人精品久久免费动漫| 国产三级久久久精品麻豆三级 | 91麻豆国产精品91久久久| 青青青青久久精品国产h久久精品五福影院1421 |