【翻譯】Delphi中類的逆向工程
標 題:【翻譯】Delphi中類的逆向工程
發信人:firstrose
時 間:2004-12-09,18:37
詳細資訊:
前段時間看到NB王(就是說nbw:D……喂,nbw你不生氣吧?)轉的文章,覺得很有用。於是想翻譯一下。無奈本人太懶,拖到現在。不過總算在今天晚飯以前趕完了:D
沒有完全按照原文翻譯,有些地方是意譯的。大家湊合看吧。原貼地址是http://www.apriorit.com/our-articles/classes-restoration.html
注意:原文版權歸原文作者所有,譯文版權由譯者所有。請尊重譯者的勞動,不得轉貼!但得到譯者明確同意的情況除外!
==============================================================
類的逆向工程
譯者:firstrose
類的逆向工程是一項需要OOP相關知識以及特定編譯器如何處理OOP部分的知識的複雜工作。
我們的任務是得到類、方法和成員。由於在用Delphi編寫的程式裡查詢類相對容易,這裡就用Delphi做示範。
類的逆向先要從查詢建構函式開始。因為類在這裡被分配記憶體,而且我們可以從中得到建構函式的一些資訊。
在Delphi程式裡找一個建構函式很簡單,只需要查詢類名出現的地方即可。
例如,對於TList可以找到下面的結構:
CODE:0040D598 TList dd offset TList_VTBL
CODE:0040D59C dd 7 dup(0)
CODE:0040D5B8 dd offset aTlist ; "TList"
CODE:0040D5BC SizeOfObject dd 10h
CODE:0040D5C0 dd offset off_4010C8
CODE:0040D5C4 dd offset TObject::SafeCallException
CODE:0040D5C8 dd offset nullsub_8
CODE:0040D5CC dd offset TObject::NewInstance
CODE:0040D5D0 dd offset TObject::FreeInstance
CODE:0040D5D4 dd offset sub_40EA08
CODE:0040D5D8 TList_VTBL dd offset TList::Grow
CODE:0040D5DC dd offset unknown_libname_107
CODE:0040D5E0 aTlist db 5,'TList'
我們把這個結構稱為“object descriptor”,即“物件描述符”。指向它的指標
被傳遞給建構函式,建構函式則從中取得建立物件所需要的資料。透過查詢對40D598
的交叉引用,可以得到對建構函式的所有呼叫。
下面是其中的一個:
CODE:0040E72E mov eax, ds:TList
CODE:0040E733 call CreateClass
CODE:0040E738 mov ds:dword_4A45F8, eax
這裡的建構函式的名字是我們自己起的。透過檢視函式,我們可以知道它是否真的是一個建構函式(CreateClass)
CODE:00402F48 CreateClass proc near ; CODE XREF: @BeginGlobalLoading+17p
CODE:00402F48 ; @CollectionsEqual+48p ...
CODE:00402F48 test dl, dl
CODE:00402F4A jz short loc_402F54
CODE:00402F4C add esp, 0FFFFFFF0h
CODE:00402F4F call __linkproc__ ClassCreate
CODE:00402F54
CODE:00402F54 loc_402F54: ; CODE XREF: CreateClass+2j
CODE:00402F54 test dl, dl
CODE:00402F56 jz short locret_402F62
CODE:00402F58 pop large dword ptr fs:0
CODE:00402F5F add esp, 0Ch
CODE:00402F62
CODE:00402F62 locret_402F62: ; CODE XREF: CreateClass+Ej
CODE:00402F62 retn
CODE:00402F62 CreateClass endp
也就是說,如果函式里有 __linkproc__ ClassCreate ,它就是一個建構函式。
下面讓我們看看生成類例項的時候發生了什麼特別的事。
CODE:00403200 __linkproc__ ClassCreate proc near ; CODE XREF: CreateClass+7p
CODE:00403200 ; sub_40AA58+Ap ...
CODE:00403200
CODE:00403200 arg_0 = dword ptr 10h
CODE:00403200
CODE:00403200 push edx
CODE:00403201 push ecx
CODE:00403202 push ebx
CODE:00403203 call dword ptr [eax-0Ch]
CODE:00403206 xor edx, edx
CODE:00403208 lea ecx, [esp+arg_0]
CODE:0040320C mov ebx, fs:[edx]
CODE:0040320F mov [ecx], ebx
CODE:00403211 mov [ecx+8], ebp
CODE:00403214 mov dword ptr [ecx+4], offset loc_403225
CODE:0040321B mov [ecx+0Ch], eax
CODE:0040321E mov fs:[edx], ecx
CODE:00403221 pop ebx
CODE:00403222 pop ecx
CODE:00403223 pop edx
CODE:00403224 retn
CODE:00403224 __linkproc__ ClassCreate endp
好的,指令
CODE:0040E72E mov eax, ds:TList
把TList結構(也就是TList_VTBL)的地址放到EAX裡。
由於我們使用的是Delphi,可以看到,這裡使用了Borland的fastcall呼叫模式(引數按照以下次序傳遞:EAX,ECX,EDX和堆疊)。這意味著,指向虛方法表的指標是作為CreateClass的第一個引數傳遞的。此外,EAX在__linkproc__ClassCreate裡沒有改變。我們可以看到:
CODE:00403203 call dword ptr [eax-0Ch]
它呼叫了什麼呢?指向TList_VTBL=0х40D5D8的指標依然在EAX裡,即
CODE:0040D5CC dd offset TObject::NewInstance
這是父類的建構函式。可以看到,TList繼承了TObject。進去看看:
CODE:00402F0C TObject::NewInstance proc near ; DATA XREF: CODE:004010FCo
CODE:00402F0C ; CODE:004011DCo ...
CODE:00402F0C push eax
CODE:00402F0D mov eax, [eax-1Ch]
CODE:00402F10 call __linkproc__ GetMem
CODE:00402F15 mov edx, eax
CODE:00402F17 pop eax
CODE:00402F18 jmp TObject::InitInstance
CODE:00402F18 TObject::NewInstance endp
EAX的值還是一樣的:0x40D5D8-0x1C=0x40D5BC。這樣物件的大小被儲存在0x40D5BC裡並傳遞給GetMem。
CODE:0040D5BC SizeOfObject dd 10h
可以看到,這裡物件的大小是0x10。
TObject::InitInstance只是將物件所在區域用0填充後,設定了剛建立的物件中指向VTBL的指標。並沒有做什麼特別的工作。
然後CreateClass就結束了。EAX中返回了指向剛剛建立的物件的指標。
這樣,對建構函式的呼叫看起來就象下面這樣:
CODE:0040E72E mov eax, ds:TList
CODE:0040E733 call CreateClass
CODE:0040E738 mov ds:dword_4A45F8, eax
分析物件的結構
我們現在知道物件所佔記憶體的大小是0x10,其中的4個位元組是VTBL的指標。但是還剩下0xC個包含了物件成員的位元組,我們必須找出它們。這裡就有點直覺的成分了。首先,物件從來不會無緣無故被建立。物件的成員,或者由建構函式賦值(可能是全部,也可能是一部分),或者由相應的設定方法來賦值。
由於TList在建構函式里被以0填充(具體在TObject::InitInstance中),在建構函式里就找不到類成員的有關資訊。Thus let’s trace life cycle after the creation.
在本例中,指向物件例項的指標被放在全域性變數dword_4A45F8裡,所以我們只需要在dword_4A45F8下個讀取記憶體斷點就可以看到類成員被呼叫了。
第一次中斷:
CODE:0041319D mov eax, [ebp+var_4]
CODE:004131A0 mov edx, ds:pTList
CODE:004131A6 mov [eax+30h], edx ; 複製的指向物件的指標
CODE:004131A9 jmp short loc_4131BD
.............
CODE:004131BD
CODE:004131BD loc_4131BD: ; CODE XREF: sub_4130BC+EDj
CODE:004131BD xor eax, eax
CODE:004131BF push ebp
CODE:004131C0 push offset loc_413276
CODE:004131C5 push dword ptr fs:[eax]
CODE:004131C8 mov fs:[eax], esp
CODE:004131CB mov eax, [ebp+var_4]
CODE:004131CE mov edx, [eax+18h]
CODE:004131D1 mov eax, [ebp+var_4]
CODE:004131D4 mov eax, [eax+30h] ;隱含地傳遞了指向物件的指標
CODE:004131D7 call Classes::TList::Add(void *)
現在看看Classes::TList::Add:
CODE:0040EA28 __fastcall Classes::TList::Add(void *) proc near
CODE:0040EA28 ; CODE XREF: @RegisterClass+9Bp
CODE:0040EA28 ; @RegisterIntegerConsts+20p ...
CODE:0040EA28 push ebx
CODE:0040EA29 push esi
CODE:0040EA2A push edi
CODE:0040EA2B mov edi, edx
CODE:0040EA2D mov ebx, eax ;可以看作是This的另一種形式
CODE:0040EA2F mov esi, [ebx+8] ; addressing to the object member №1
CODE:0040EA32 cmp esi, [ebx+0Ch] ; addressing to the object member №3
CODE:0040EA35 jnz short loc_40EA3D
CODE:0040EA37 mov eax, ebx
CODE:0040EA39 mov edx, [eax] ;addressing to TList->pVTBL
CODE:0040EA3B call dword ptr [edx]
CODE:0040EA3D
CODE:0040EA3D loc_40EA3D: ; CODE XREF: Classes::TList::Add(void *)+Dj
CODE:0040EA3D mov eax, [ebx+4] ; addressing to the object member №2
CODE:0040EA40 mov [eax+esi*4], edi
CODE:0040EA43 inc dword ptr [ebx+8]
CODE:0040EA46 mov eax, esi
CODE:0040EA48 pop edi
CODE:0040EA49 pop esi
CODE:0040EA4A pop ebx
CODE:0040EA4B retn
CODE:0040EA4B __fastcall Classes::TList::Add(void *) endp
好了,最後的3個成員找到了。它們都是4位元組長。
要使用IDA分析類的工作變得簡單一點,可以使用結構體功能。實際上,類和結構是一樣的:)))
用了下面的結構定義以後:
00000000 TList_obj struc ; (大小=0X10)
00000000 pVTBL dd ?
00000004 Property1 dd ?
00000008 Property2 dd ?
0000000C Property3 dd ?
00000010 TList_obj ends
程式碼清晰多了:
CODE:0040EA28 __fastcall Classes::TList::Add(void *) proc near
CODE:0040EA28 ; CODE XREF: @RegisterClass+9Bp
CODE:0040EA28 ; @RegisterIntegerConsts+20p ...
CODE:0040EA28 push ebx
CODE:0040EA29 push esi
CODE:0040EA2A push edi
CODE:0040EA2B mov edi, edx
CODE:0040EA2D mov ebx, eax
CODE:0040EA2F mov esi, [ebx+TList_obj.Property2]
CODE:0040EA32 cmp esi, [ebx+TList_obj.Property3]
CODE:0040EA35 jnz short loc_40EA3D
CODE:0040EA37 mov eax, ebx
CODE:0040EA39 mov edx, [eax+TList_obj.pVTBL]
CODE:0040EA3B call dword ptr [edx] ;TList::Grow
CODE:0040EA3D
CODE:0040EA3D loc_40EA3D: ; CODE XREF: Classes::TList::Add(void *)+Dj
CODE:0040EA3D mov eax, [ebx+TList_obj.Property1]
CODE:0040EA40 mov [eax+esi*4], edi
CODE:0040EA43 inc [ebx+TList_obj.Property2]
CODE:0040EA46 mov eax, esi
CODE:0040EA48 pop edi
CODE:0040EA49 pop esi
CODE:0040EA4A pop ebx
CODE:0040EA4B retn
CODE:0040EA4B __fastcall Classes::TList::Add(void *) endp
考慮到VBTL的結構,很容易想到:
CODE:0040EA3B call dword ptr [edx]
就是TList::Grow,
因為
CODE:0040D5D8 pVTBL dd offset TList::Grow
現在我們可以對類的成員做一點深入的分析了。比方說,看到下面的程式碼:
CODE:0040EA3D mov eax, [ebx+TList_obj.Property1]
CODE:0040EA40 mov [eax+esi*4], edi
CODE:0040EA43 inc [ebx+TList_obj.Property2]
就可以知道Property2是TList中元素的計數器。因為增加一個元素時,它也被加一。Property1是指向元素陣列的指標。Property 2可以看作是陣列的索引。而Property 3則是一個list裡最多允許的元素數目。此外,只有當Property2等於Property3時,TList::Grow被呼叫。透過邏輯推理,我們知道了這些。現在,一切都清楚起來了。順便看看幫助文件,給這些成員命名吧:
CODE:0040EA28 __fastcall Classes::TList::Add(void *) proc near
CODE:0040EA28 ; CODE XREF: @RegisterClass+9Bp
CODE:0040EA28 ; @RegisterIntegerConsts+20p ...
CODE:0040EA28 push ebx
CODE:0040EA29 push esi
CODE:0040EA2A push edi
CODE:0040EA2B mov edi, edx
CODE:0040EA2D mov ebx, eax
CODE:0040EA2F mov esi, [ebx+TList_obj.Count]
CODE:0040EA32 cmp esi, [ebx+TList_obj.Capacity]
CODE:0040EA35 jnz short loc_40EA3D
CODE:0040EA37 mov eax, ebx
CODE:0040EA39 mov edx, [eax+TList_obj.pVTBL]
CODE:0040EA3B call dword ptr [edx]
CODE:0040EA3D
CODE:0040EA3D loc_40EA3D: ; CODE XREF: Classes::TList::Add(void *)+Dj
CODE:0040EA3D mov eax, [ebx+TList_obj.Items]
CODE:0040EA40 mov [eax+esi*4], edi
CODE:0040EA43 inc [ebx+TList_obj.Count]
CODE:0040EA46 mov eax, esi
CODE:0040EA48 pop edi
CODE:0040EA49 pop esi
CODE:0040EA4A pop ebx
CODE:0040EA4B retn
CODE:0040EA4B __fastcall Classes::TList::Add(void *) endp
物件的結構已經分析好了,下面是物件成員。
查詢物件方法
物件的方法可以是以下幾種:公開/私有(保護),虛方法/非虛方法以及靜態方法.
由於編譯後的靜態方法和普通的過程沒有什麼區別,所以靜態方法是無法被識別的。這些函式和某個特定的類之間的關係也是無法確定的。但是,應該指出的是,如果某個靜態方法在類的方法裡被呼叫,那麼,它是可見的。否則尋找靜態方法的企圖只是在浪費時間。
虛方法很容易找到――它們都位於VTBL裡。但是我們應該如何查詢一般的方法呢?想想OOP:當物件方法被呼叫時,指向物件本身的指標被隱含地傳遞給該方法。實際上,這就意味著每個方法的第一個引數就是指向物件的指標。也就是說,如果該方法被宣告為fastcall型別,指向物件的指標是放在EAX裡的。而對於cdecl或stdcall型別的方法,首個引數是放在堆疊裡的。讓我們來看看指向物件的指標被放在什麼地方……好!在dword_4A45F8裡。透過查詢對4A45F8的交叉引用,我們可以找到很多非虛擬方法。我們還可以在4A45F8下一個斷點,追蹤對物件例項指標的複製以找出餘下的方法。
在本例中,由於使用了全域性變數,一切都很容易。但是如果使用的是區域性變數或者程式碼無法被執行(比如說,一個驅動程式。或者該程式碼不允許被執行),又應該怎麼做呢?這就需要一個特別的辦法。
一步一步來:
1)首先要找到所有呼叫建構函式的地方。
對每個呼叫重複以下步驟
2)跟去看看指向當前物件例項的指標被寫到哪裡了。
3)把所有呼叫了建構函式的函式作為物件方法。
4)如果沒有這樣的函式呼叫,就看建構函式下面的一個呼叫。否則就檢視所有對已經找到的方法的交叉引用。這樣就可以找到不在建構函式附近的呼叫。由於我們已經知道方法的首個引數是指向物件本身的指標,於是就可以查詢物件指標的交叉引用。用這樣的方法,我們可以一層一層地分析程式碼,直到出現僵局或者找到物件方法。
5)分析下一個已經找到的方法。
例如,我們已經找到了Classes::TList::Add,而且也找到了對Classes::TList::Add的一個引用:
CODE:0040F020 TThreadList::Add proc near ; CODE XREF: TCanvas::`...'+9Ep
CODE:0040F020 ; Graphics::_16725+C4p
CODE:0040F020
CODE:0040F020 var_4 = dword ptr -4
CODE:0040F020
CODE:0040F020 push ebp
CODE:0040F021 mov ebp, esp
CODE:0040F023 push ecx
CODE:0040F024 push ebx
CODE:0040F025 mov ebx, edx
CODE:0040F027 mov [ebp+var_4], eax
CODE:0040F02A mov eax, [ebp+var_4]
CODE:0040F02D call TThreadList::LockList
CODE:0040F032 xor eax, eax
CODE:0040F034 push ebp
CODE:0040F035 push offset loc_40F073
CODE:0040F03A push dword ptr fs:[eax]
CODE:0040F03D mov fs:[eax], esp
CODE:0040F040 mov eax, [ebp+var_4]
CODE:0040F043 mov eax, [eax+4]
CODE:0040F046 mov edx, ebx
CODE:0040F048 call TList::IndexOf
CODE:0040F04D inc eax
CODE:0040F04E jnz short loc_40F05D
CODE:0040F050 mov eax, [ebp+var_4]
CODE:0040F053 mov eax, [eax+4]
CODE:0040F056 mov edx, ebx
CODE:0040F058 call Classes::TList::Add(void *)
就是說,我們找到了TList::IndexOf方法。
進一步分析發現,我們處在TthreadList物件的方法中,TList是它的成員之一。這裡沒有什麼可以看的東西。假定一下,沒有其他對Classes::TList::Add的引用。進到TList::IndexOf方法並且檢視對它的引用。下面是其中的一個:
CODE:0040EE38 TList::Remove proc near ; CODE XREF: TThreadList::Remove+28p
CODE:0040EE38 ; TCollection::RemoveItem+Bp ...
CODE:0040EE38 push ebx
CODE:0040EE39 push esi
CODE:0040EE3A mov ebx, eax
CODE:0040EE3C mov eax, ebx
CODE:0040EE3E call TList::IndexOf
CODE:0040EE43 mov esi, eax
CODE:0040EE45 cmp esi, 0FFFFFFFFh
CODE:0040EE48 jz short loc_40EE53
CODE:0040EE4A mov edx, esi
CODE:0040EE4C mov eax, ebx
CODE:0040EE4E call TList::Delete
CODE:0040EE53
CODE:0040EE53 loc_40EE53: ; CODE XREF: TList::Remove+10j
CODE:0040EE53 mov eax, esi
CODE:0040EE55 pop esi
CODE:0040EE56 pop ebx
CODE:0040EE57 retn
CODE:0040EE57 TList::Remove endp
這樣,TList::Delete和TList::Remove就有了。
下面就是所有物件指標的交叉引用和相關變數。
這裡是查詢變數的例子:
CODE:0041319D mov eax, [ebp+var_4]
CODE:004131A0 mov edx, ds:pTList
CODE:004131A6 mov [eax+30h], edx ;物件指標
CODE:004131A9 jmp short loc_4131BD
下面可以看到:
CODE:00413236 mov eax, [eax+30h]
CODE:00413239 mov edx, [ebp+var_10]
CODE:0041323C call TList::Get
如何分辨公開方法和私有方法呢?
只有當所有的方法全部找到以後才可以做這件事。私有方法只有在其它方法裡才有呼叫。就是說,必須檢視交叉引用了。查詢方法以前,建議先把它們編號。也即把你找到的方法依次命名為Object1::Method1,Object1::Method2……所有的方法全部出來以後,就可以開始分析它們的引數(主要是個數和型別)了。
確定方法引數的個數
關於cdecl和stdcall幾乎沒有什麼可說的。只要把IDA找到的引數個數減去1就可以了(還記得嗎?第一個引數是物件指標,其它的才是真正的引數)。
fastcall要複雜點兒。首先我們要記住引數的次序:EAX,EDX,ECX,堆疊。首先要看看IDA找到了幾個透過堆疊傳遞的引數。如果至少有一個,那麼引數的個數要加3(3個暫存器引數加上堆疊引數)。由於第一個引數是物件指標This,這個數目還要減去1才是真正的引數個數。如果沒有堆疊引數的話,就要看看函式的開頭了。由於Delphi試圖不去攪亂暫存器的值,結果每個fastcall函式的開頭都要儲存EAX,EDX和ECX:
mov esi, edx ; 第一個引數
mov ebx, eax ; This指標
mov edi, ecx ; 第二個引數
根據被複制的暫存器個數就可以判斷出引數的個數。比如:
CODE:0040EBE0 TList::Get proc near ; CODE XREF: @GetClass+1Dp
CODE:0040EBE0 ; @UnRegisterModuleClasses+24p ...
CODE:0040EBE0
CODE:0040EBE0 var_4 = dword ptr -4
CODE:0040EBE0
CODE:0040EBE0 push ebp
CODE:0040EBE1 mov ebp, esp
CODE:0040EBE3 push 0
CODE:0040EBE5 push ebx
CODE:0040EBE6 push esi
CODE:0040EBE7 mov esi, edx
CODE:0040EBE9 mov ebx, eax
CODE:0040EBEB xor eax, eax
一共2個引數,其中一個是This指標。那麼TList::Get有1個引數。
CODE:004198CC push ebp
CODE:004198CD mov ebp, esp
CODE:004198CF add esp, 0FFFFFF8Ch
CODE:004198D2 push ebx
CODE:004198D3 push esi
CODE:004198D4 push edi
CODE:004198D5 mov [ebp+var_C], ecx
CODE:004198D8 mov [ebp+var_8], edx
CODE:004198DB mov [ebp+var_4], eax
一共3個引數,其中一個是This指標。那麼真正的引數是2個。
值得指出的是,由於我們是在用IDA分析Delphi程式,基於上面的原因,寫函式頭時一定要考慮到物件指標This。
引數的型別就要靠你去分析了。
================================================================
相關文章
- [Flutter翻譯]通過重新編譯Flutter引擎對Flutter應用進行逆向工程。2021-03-15Flutter編譯
- 【翻譯】c++類中“空成員”的優化2022-05-26C++優化
- js逆向之——百度翻譯介面2024-06-24JS
- 1.某道翻譯js逆向sign值2024-04-21JS
- 人工翻譯的分類 安睿傑線上翻譯平臺2022-08-11
- 微軟機器翻譯系統:中-英翻譯水平可“與人類媲美”2018-03-15微軟
- [翻譯]Windows Exploit開發教程第十三章Part1.IE10-IE逆向工程2020-02-11WindowsIE10
- mybatis的逆向工程2020-10-03MyBatis
- Delphi TDictionary字典類2024-03-12
- aardio爬蟲) 實戰篇:逆向有道翻譯web介面2024-05-06爬蟲Web
- 【JS 逆向百例】cnki 學術翻譯 AES 加密分析2021-11-18JS加密
- [Flutter翻譯]Flutter中的剪下2020-07-23Flutter
- 中國翻譯協會:2022中國翻譯人才發展2022-04-01
- [Web翻譯]JavaScript中的編譯與填充2020-07-27WebJavaScript編譯
- 小白的MyBatis逆向工程2020-05-01MyBatis
- Easy Translator語言翻譯類工具2022-04-06R語言
- 口譯翻譯類別及服務內容2022-09-07
- MyEclipse2014使用Hibernate逆向工程生成實體類2018-05-27Eclipse
- Mybatis逆向工程和新版本MybatisPlus3.4逆向工程的使用2021-10-01MyBatisS3
- Mybatis逆向工程2024-03-10MyBatis
- oc-plugin-book 文件協作翻譯外掛(類似 LearnKu.com 的文件翻譯)2019-06-16Plugin
- 【翻譯】.NET 6 中的 dotnet monitor2021-12-06
- 翻譯 | Java 中的變型(Variance)2019-06-23Java
- 收藏的爬蟲逆向工程2018-10-17爬蟲
- [譯]記一次Kotlin官方文件翻譯的PR(內聯類)2018-12-21Kotlin
- 中文翻譯英語的軟體哪個好?如何完成中翻譯英2019-09-27
- IDEA中建立springboot+Mybatis+generator逆向工程2020-12-10IdeaSpring BootMyBatis
- 逆向工程核心原理(1)逆向基礎2023-03-16
- 漫談逆向工程2020-09-20
- 【翻譯】.NET 5中的效能改進2020-07-15
- 騰訊互動翻譯的坑爹翻譯2024-12-10
- TypeScript 官方手冊翻譯計劃【十二】:類2021-12-11TypeScript
- 【翻譯】 What is class diagram(什麼是類圖)?2019-01-26
- ibatis和myBatis的逆向工程使用2018-11-29MyBatis
- 翻譯2020-12-29
- 中國翻譯協會:2022中國翻譯及語言服務行業發展2022-04-01行業
- Arctime怎麼翻譯字幕?Arctime批次翻譯字幕的技巧2020-07-14
- Symbol 的作用[翻譯]2019-04-05Symbol
- [翻譯]JavaScript的成本2019-02-27JavaScript