抖音資料采集Frida教程,rpc、Process、Module、Memory使用方法及示例
前言
大家好,窩又來寫文章了,咱們現在在這篇文章中,我們來對其官方的一些非常常用的API進行學習,所謂工欲善其事,必先利其器,想要好好學習FRIDA我們就必須對FRIDA API深入的學習以對其有更深的了解和使用,通常大部分核心原理也在官方API中寫著,我們學會來使用一些案例來結合API的使用,
注意,運行以下任何代碼時都需要提前啟動手機中的frida-server檔案,
1.1 FRIDA輸出列印
1.1.1 console輸出
不論是什么語言都好,第一個要學習總是如何輸出和列印,那我們就來學習在FRIDA列印值,在官方API有兩種列印的方式,分別是console、send,我們先來學習非常的簡單的console,這里我創建一個js檔案,代碼示例如下,
function hello_printf() {
Java.perform(function () {
console.log("");
console.log("hello-log");
console.warn("hello-warn");
console.error("hello-error");
});
}
setImmediate(hello_printf,0);
當檔案創建好之后,我們需要運行在手機中安裝的frida-server檔案,在上一章我們學過了如何安裝在android手機安裝frida-server,現在來使用它,我們在ubuntu中開啟一個終端,運行以下代碼,啟動我們安裝好的frida-server檔案,
roysue@ubuntu:~$ adb shell
sailfish:/ $ su
sailfish:/ $ ./data/local/tmp/frida-server
然后執行以下代碼,對目標應用app的行程com.roysue.roysueapplication使用-l命令注入Chap03.js中的代碼1-1以及執行腳本之后的效果圖1-1!frida -U com.roysue.roysueapplication -l Chap03.js
代碼1-1 代碼示例

圖1-1 終端執行
可以到終點已經成功注入了腳本并且列印了hello,但是顏色不同,這是log的級別的原因,在FRIDA的console中有三個級別分別是log、warn、error,
| 級別 | 含義 |
|---|---|
| log | 正常 |
| warn | 警告 |
| error | 錯誤 |
1.1.2 console之hexdump
error級別最為嚴重其次warn,但是一般在使用中我們只會使用log來輸出想看的值;然后我們繼續學習console的好兄弟,hexdump,其含義:列印記憶體中的地址,target引數可以是ArrayBuffer或者NativePointer,而options引數則是自定義輸出格式可以填這幾個引數offset、lengt、header、ansi,hexdump代碼示例以及執行效果如下,
var libc = Module.findBaseAddress('libc.so');
console.log(hexdump(libc, {
offset: 0,
length: 64,
header: true,
ansi: true
}));
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............
00000010 03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00 ..(.........4...
00000020 34 a8 04 00 00 00 00 05 34 00 20 00 08 00 28 00 4.......4. ...(.
00000030 1e 00 1d 00 06 00 00 00 34 00 00 00 34 00 00 00 ........4...4...
1.1.3 send
send是在python層定義的on_message回呼函式,jscode內所有的資訊都被監控script.on('message', on_message),當輸出資訊的時候on_message函式會拿到其資料再通過format轉換, 其最重要的功能也是最核心的是能夠直接將資料以json格式輸出,當然資料是二進制的時候也依然是可以使用send,十分方便,我們來看代碼1-2示例以及執行效果,
# -*- coding: utf-8 -*-
import frida
import sys
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
jscode = """
Java.perform(function ()
{
var jni_env = Java.vm.getEnv();
console.log(jni_env);
send(jni_env);
});
"""
process = frida.get_usb_device().attach('com.roysue.roysueapplication')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()
運行腳本效果如下:
roysue@ubuntu:~/Desktop/Chap09$ python Chap03.py
[object Object]
[*] {'handle': '0xdf4f8000', 'vm': {}}
可以看出這里兩種方式輸出的不同的效果,console直接輸出了[object Object],無法輸出其正常的內容,因為jni_env實際上是一個物件,但是使用send的時候會自動將物件轉json格式輸出,通過對比,我們就知道send的好處啦~
1.2 FRIDA變數型別
學完輸出之后我們來學習如何宣告變數型別,
| API | 含義 |
|---|---|
| new Int64(v) | 定義一個有符號Int64型別的變數值為v,引數v可以是字串或者以0x開頭的的十六進制值 |
| new UInt64(v) | 定義一個無符號Int64型別的變數值為v,引數v可以是字串或者以0x開頭的的十六進制值 |
| new NativePointer(s) | 定義一個指標,指標地址為s |
| ptr(“0”) | 同上 |
代碼示例以及效果
Java.perform(function () {
console.log("");
console.log("new Int64(1):"+new Int64(1));
console.log("new UInt64(1):"+new UInt64(1));
console.log("new NativePointer(0xEC644071):"+new NativePointer(0xEC644071));
console.log("new ptr('0xEC644071'):"+new ptr(0xEC644071));
});
輸出效果如下:
new Int64(1):1
new UInt64(1):1
new NativePointer(0xEC644071):0xec644071
new ptr('0xEC644071'):0xec644071
frida也為Int64(v)提供了一些相關的API:
| API | 含義 |
|---|---|
| add(rhs)、sub(rhs)、and(rhs)、or(rhs)、xor(rhs) | 加、減、邏輯運算 |
| shr(N)、shl(n) | 向右/向左移位n位生成新的Int64 |
| Compare(Rhs) | 回傳整數比較結果 |
| toNumber() | 轉換為數字 |
| toString([radix=10]) | 轉換為可選基數的字串(默認為10) |
我也寫了一些使用案例,代碼如下,
function hello_type() {
Java.perform(function () {
console.log("");
//8888 + 1 = 8889
console.log("8888 + 1:"+new Int64("8888").add(1));
//8888 - 1 = 8887
console.log("8888 - 1:"+new Int64("8888").sub(1));
//8888 << 1 = 4444
console.log("8888 << 1:"+new Int64("8888").shr(1));
//8888 == 22 = 1 1是false
console.log("8888 == 22:"+new Int64("8888").compare(22));
//轉string
console.log("8888 toString:"+new Int64("8888").toString());
});
}
代碼執行效果如圖1-2,
圖1-2 Int64 API
1.3 RPC遠程呼叫
可以替換或插入的空物件,以向應用程式公開RPC樣式的API,該鍵指定方法名稱,該值是匯出的函式,此函式可以回傳一個純值以立即回傳給呼叫方,或者承諾異步回傳,也就是說可以通過rpc的匯出的功能使用在python層,使python層與js互動,官方示例代碼有Node.js版本與python版本,我們在這里使用python版本,代碼如下,
1.3.1 遠程呼叫代碼示例
import frida
def on_message(message, data):
if message['type'] == 'send':
print(message['payload'])
elif message['type'] == 'error':
print(message['stack'])
session = frida.get_usb_device().attach('com.roysue.roysueapplication')
source = """
rpc.exports = {
add: function (a, b) {
return a + b;
},
sub: function (a, b) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(a - b);
}, 100);
});
}
};
"""
script = session.create_script(source)
script.on('message', on_message)
script.load()
print(script.exports.add(2, 3))
print(script.exports.sub(5, 3))
session.detach()
1.3.2 遠程呼叫代碼示例詳解
官方原始碼示例是附加在目標行程為iTunes,再通過將rpc的./agent.js檔案讀取到source,進行使用,我這里修改了附加的目標的行程以及直接將rpc的代碼定義在source中,我們來看看這段是咋運行的,仍然先對目標行程附加,然后在寫js中代碼,也是source變數,通過rpc.exports關鍵字定義需要匯出的兩個函式,上面定義了add函式和sub函式,兩個的函式寫作方式不一樣,大家以后寫按照add方法寫就好了,sub稍微有點復雜,宣告完函式之后創建了一個腳本并且注入行程,加載了腳本之后可以到print(script.exports.add(2, 3))以及print(script.exports.sub(5, 3)),在python層直接呼叫,add的回傳的結果為5,sub則是2,下見下圖1-3,
圖1-3 執行python腳本
1.4 Process物件
我們現在來介紹以及使用一些Process物件中比較常用的api~
1.4.1 Process.id
Process.id:回傳附加目標行程的PID
1.4.2 Process.isDebuggerAttached()
Process.isDebuggerAttached():檢測當前是否對目標程式已經附加
1.4.3 Process.enumerateModules()
列舉當前加載的模塊,回傳模塊物件的陣列,Process.enumerateModules()會列舉當前所有已加載的so模塊,并且回傳了陣列Module物件,Module物件下一節我們來詳細說,在這里我們暫時只使用Module物件的name屬性,
function frida_Process() {
Java.perform(function () {
var process_Obj_Module_Arr = Process.enumerateModules();
for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
console.log("",process_Obj_Module_Arr[i].name);
}
});
}
setImmediate(frida_Process,0);
我來們開看看這段js代碼寫了啥:在js中能夠直接使用Process物件的所有api,呼叫了Process.enumerateModules()方法之后會回傳一個陣列,陣列中存盤N個叫Module的物件,既然已經知道回傳了的是一個陣列,很簡單我們就來for回圈它便是,這里我使用下標的方式呼叫了Module物件的name屬性,name是so模塊的名稱,見下圖1-4,
圖1-4 終端輸出了所有已加載的so
1.4.4 Process.enumerateThreads()
Process.enumerateThreads():列舉當前所有的執行緒,回傳包含以下屬性的物件陣列:
| 屬性 | 含義 |
|---|---|
| id | 執行緒id |
| state | 當前運行狀態有running, stopped, waiting, uninterruptible or halted |
| context | 帶有鍵pc和sp的物件,它們是分別為ia32/x64/arm指定EIP/RIP/PC和ESP/RSP/SP的NativePointer物件,也可以使用其他處理器特定的密鑰,例如eax、rax、r0、x0等, |
使用代碼示例如下:
function frida_Process() {
Java.perform(function () {
var enumerateThreads = Process.enumerateThreads();
for(var i = 0; i < enumerateThreads.length; i++) {
console.log("");
console.log("id:",enumerateThreads[i].id);
console.log("state:",enumerateThreads[i].state);
console.log("context:",JSON.stringify(enumerateThreads[i].context));
}
});
}
setImmediate(frida_Process,0);
獲取當前是所有執行緒之后回傳了一個陣列,然后回圈輸出它的值,如下圖1-5,
圖1-4 終端執行
1.4.5 Process.getCurrentThreadId()
Process.getCurrentThreadId():獲取此執行緒的作業系統特定 ID 作為數字
1.5 Module物件
3.4章節中Process.EnumererateModules()方法回傳了就是一個Module物件,咱們這里來詳細說說Module物件,先來瞧瞧它都有哪些屬性,
1.5.1 Module物件的屬性
| 屬性 | 含義 |
|---|---|
| name | 模塊名稱 |
| base | 模塊地址,其變數型別為NativePointer |
| size | 大小 |
| path | 完整檔案系統路徑 |
除了屬性我們再來看看它有什么方法,
1.5.2 Module物件的API
| API | 含義 |
|---|---|
| Module.load() | 加載指定so檔案,回傳一個Module物件 |
| enumerateImports() | 列舉所有Import庫函式,回傳Module陣列物件 |
| enumerateExports() | 列舉所有Export庫函式,回傳Module陣列物件 |
| enumerateSymbols() | 列舉所有Symbol庫函式,回傳Module陣列物件 |
| Module.findExportByName(exportName)、Module.getExportByName(exportName) | 尋找指定so中export庫中的函式地址 |
| Module.findBaseAddress(name)、Module.getBaseAddress(name) | 回傳so的基地址 |
1.5.3 Module.load()
在frida-12-5版本中更新了該API,主要用于加載指定so檔案,回傳一個Module物件,
使用代碼示例如下:
function frida_Module() {
Java.perform(function () {
//引數為so的名稱 回傳一個Module物件
const hooks = Module.load('libhello.so');
//輸出
console.log("模塊名稱:",hooks.name);
console.log("模塊地址:",hooks.base);
console.log("大小:",hooks.size);
console.log("檔案系統路徑",hooks.path);
});
}
setImmediate(frida_Module,0);
輸出如下:
模塊名稱: libhello.so
模塊地址: 0xdf2d3000
大小: 24576
檔案系統路徑 /data/app/com.roysue.roysueapplication-7adQZoYIyp5t3G5Ef5wevQ==/lib/arm/libhello.so
1.5.4 Process.EnumererateModules()
咱們這一小章節就來使用Module物件,把上章的Process.EnumererateModules()物件輸出給它補全了,代碼如下,
function frida_Module() {
Java.perform(function () {
var process_Obj_Module_Arr = Process.enumerateModules();
for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
if(process_Obj_Module_Arr[i].path.indexOf("hello")!=-1)
{
console.log("模塊名稱:",process_Obj_Module_Arr[i].name);
console.log("模塊地址:",process_Obj_Module_Arr[i].base);
console.log("大小:",process_Obj_Module_Arr[i].size);
console.log("檔案系統路徑",process_Obj_Module_Arr[i].path);
}
}
});
}
setImmediate(frida_Module,0);
輸出如下:
模塊名稱: libhello.so
模塊地址: 0xdf2d3000
大小: 24576
檔案系統路徑 /data/app/com.roysue.roysueapplication-7adQZoYIyp5t3G5Ef5wevQ==/lib/arm/libhello.so
這邊如果去除判斷的話會列印所有加載的so的資訊,這里我們就知道了哪些方法回傳了Module物件了,然后我們再繼續深入學習Module物件自帶的API,
1.5.5 enumerateImports()
該API會列舉模塊中所有中的所有Import函式,示例代碼如下,
function frida_Module() {
Java.perform(function () {
const hooks = Module.load('libhello.so');
var Imports = hooks.enumerateImports();
for(var i = 0; i < Imports.length; i++) {
//函式型別
console.log("type:",Imports[i].type);
//函式名稱
console.log("name:",Imports[i].name);
//屬于的模塊
console.log("module:",Imports[i].module);
//函式地址
console.log("address:",Imports[i].address);
}
});
}
setImmediate(frida_Module,0);
輸出如下:
[Google Pixel::com.roysue.roysueapplication]-> type: function
name: __cxa_atexit
module: /system/lib/libc.so
address: 0xf58f4521
type: function
name: __cxa_finalize
module: /system/lib/libc.so
address: 0xf58f462d
type: function
name: __stack_chk_fail
module: /system/lib/libc.so
address: 0xf58e2681
...
1.5.6 enumerateExports()
該API會列舉模塊中所有中的所有Export函式,示例代碼如下,
function frida_Module() {
Java.perform(function () {
const hooks = Module.load('libhello.so');
var Exports = hooks.enumerateExports();
for(var i = 0; i < Exports.length; i++) {
//函式型別
console.log("type:",Exports[i].type);
//函式名稱
console.log("name:",Exports[i].name);
//函式地址
console.log("address:",Exports[i].address);
}
});
}
setImmediate(frida_Module,0);
輸出如下:
[Google Pixel::com.roysue.roysueapplication]-> type: function
name: Java_com_roysue_roysueapplication_hellojni_getSum
address: 0xdf2d411b
type: function
name: unw_save_vfp_as_X
address: 0xdf2d4c43
type: function
address: 0xdf2d4209
type: function
...
1.5.7 enumerateSymbols()
代碼示例如下,
function frida_Module() {
Java.perform(function () {
const hooks = Module.load('libc.so');
var Symbol = hooks.enumerateSymbols();
for(var i = 0; i < Symbol.length; i++) {
console.log("isGlobal:",Symbol[i].isGlobal);
console.log("type:",Symbol[i].type);
console.log("section:",JSON.stringify(Symbol[i].section));
console.log("name:",Symbol[i].name);
console.log("address:",Symbol[i].address);
}
});
}
setImmediate(frida_Module,0);
輸出如下:
isGlobal: true
type: function
section: {"id":"13.text","protection":"r-x"}
name: _Unwind_GetRegionStart
address: 0xf591c798
isGlobal: true
type: function
section: {"id":"13.text","protection":"r-x"}
name: _Unwind_GetTextRelBase
address: 0xf591c7cc
...
1.5.8 Module.findExportByName(exportName), Module.getExportByName(exportName)
回傳so檔案中Export函式庫中函式名稱為exportName函式的絕對地址,
代碼示例如下,
function frida_Module() {
Java.perform(function () {
Module.getExportByName('libhello.so', 'c_getStr')
console.log("Java_com_roysue_roysueapplication_hellojni_getStr address:",Module.findExportByName('libhello.so', 'Java_com_roysue_roysueapplication_hellojni_getStr'));
console.log("Java_com_roysue_roysueapplication_hellojni_getStr address:",Module.getExportByName('libhello.so', 'Java_com_roysue_roysueapplication_hellojni_getStr'));
});
}
setImmediate(frida_Module,0);
輸出如下:
Java_com_roysue_roysueapplication_hellojni_getStr address: 0xdf2d413d
Java_com_roysue_roysueapplication_hellojni_getStr address: 0xdf2d413d
1.5.9 Module.findBaseAddress(name)、Module.getBaseAddress(name)
回傳name模塊的基地址,
代碼示例如下,
function frida_Module() {
Java.perform(function () {
var name = "libhello.so";
console.log("so address:",Module.findBaseAddress(name));
console.log("so address:",Module.getBaseAddress(name));
});
}
setImmediate(frida_Module,0);
輸出如下:
so address: 0xdf2d3000
so address: 0xdf2d3000
1.6 Memory物件
Memory的一些API通常是對記憶體處理,譬如Memory.copy()復制記憶體,又如writeByteArray寫入位元組到指定記憶體中,那我們這章中就是學習使用Memory API向記憶體中寫入資料、讀取資料,
1.6.1 Memory.scan搜索記憶體資料
其主要功能是搜索記憶體中以address地址開始,搜索長度為size,需要搜是條件是pattern,callbacks搜索之后的回呼函式;此函式相當于搜索記憶體的功能,
我們來直接看例子,然后結合例子講解,如下圖1-5,
圖1-5 IDA中so檔案某處資料
如果我想搜索在記憶體中112A地址的起始資料要怎么做,代碼示例如下,
function frida_Memory() {
Java.perform(function () {
//先獲取so的module物件
var module = Process.findModuleByName("libhello.so");
//??是通配符
var pattern = "03 49 ?? 50 20 44";
//基址
console.log("base:"+module.base)
//從so的基址開始搜索,搜索大小為so檔案的大小,搜指定條件03 49 ?? 50 20 44的資料
var res = Memory.scan(module.base, module.size, pattern, {
onMatch: function(address, size){
//搜索成功
console.log('搜索到 ' +pattern +" 地址是:"+ address.toString());
},
one rror: function(reason){
//搜索失敗
console.log('搜索失敗');
},
onComplete: function()
{
//搜索完畢
console.log("搜索完畢")
}
});
});
}
setImmediate(frida_Memory,0);
先來看看回呼函式的含義,onMatch:function(address,size):使用包含作為NativePointer的實體地址的address和指定大小為數字的size呼叫,此函式可能會回傳字串STOP以提前取消記憶體掃描,onError:Function(Reason):當掃描時出現記憶體訪問錯誤時使用原因呼叫,onComplete:function():當記憶體范圍已完全掃描時呼叫,
我們來來說上面這段代碼做了什么事情:搜索libhello.so檔案在記憶體中的資料,搜索以pattern條件的在記憶體中能匹配的資料,搜索到之后根據回呼函式回傳資料,
我們來看看執行之后的效果圖1-6,
圖1-6 終端執行
我們要如何驗證搜索到底是不是圖1-5中112A地址,其實很簡單,so的基址是0xdf2d3000,而搜到的地址是0xdf2d412a,我們只要df2d412a-df2d3000=112A,就是說我們已經搜索到了!
1.6.2 搜索記憶體資料Memory.scanSync
功能與Memory.scan一樣,只不過它是回傳多個匹配到條件的資料,
代碼示例如下,
function frida_Memory() {
Java.perform(function () {
var module = Process.findModuleByName("libhello.so");
var pattern = "03 49 ?? 50 20 44";
var scanSync = Memory.scanSync(module.base, module.size, pattern);
console.log("scanSync:"+JSON.stringify(scanSync));
});
}
setImmediate(frida_Memory,0);
輸出如下,可以看到地址搜索出來是一樣的
scanSync:[{"address":"0xdf2d412a","size":6}]
1.6.3 記憶體分配Memory.alloc
在目標行程中的堆上申請size大小的記憶體,并且會按照Process.pageSize對齊,回傳一個NativePointer,并且申請的記憶體如果在JavaScript里面沒有對這個記憶體的使用的時候會自動釋放的,也就是說,如果你不想要這個記憶體被釋放,你需要自己保存一份對這個記憶體塊的參考,
使用案例如下
function frida_Memory() {
Java.perform(function () {
const r = Memory.alloc(10);
console.log(hexdump(r, {
offset: 0,
length: 10,
header: true,
ansi: false
}));
});
}
setImmediate(frida_Memory,0);
以上代碼在目標行程中申請了10位元組的空間<br>可以看到在`0xdfe4cd40`處申請了`10`個位元組記憶體空間
也可以使用:Memory.allocUtf8String(str) 分配utf字串Memory.allocUtf16String 分配utf16字串Memory.allocAnsiString 分配ansi字串
1.6.4 記憶體復制Memory.copy
如同c api memcp一樣呼叫,使用案例如下,
function frida_Memory() {
Java.perform(function () {
//獲取so模塊的Module物件
var module = Process.findModuleByName("libhello.so");
//條件
var pattern = "03 49 ?? 50 20 44";
//搜字串 只是為了將so的記憶體資料復制出來 方便演示~
var scanSync = Memory.scanSync(module.base, module.size, pattern);
//申請一個記憶體空間大小為10個位元組
const r = Memory.alloc(10);
//復制以module.base地址開始的10個位元組 那肯定會是7F 45 4C 46...因為一個ELF檔案的Magic屬性如此,
Memory.copy(r,module.base,10);
console.log(hexdump(r, {
offset: 0,
length: 10,
header: true,
ansi: false
}));
});
}
setImmediate(frida_Memory,0);
輸出如下,
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
e8142070 7f 45 4c 46 01 01 01 00 00 00 .ELF......
從module.base中復制10個位元組的記憶體到新年申請的r內
1.6.6 寫入記憶體Memory.writeByteArray
將位元組陣列寫入一個指定記憶體,代碼示例如下:
function frida_Memory() {
Java.perform(function () {
//定義需要寫入的位元組陣列 這個位元組陣列是字串"roysue"的十六進制
var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];
//申請一個新的記憶體空間 回傳指標 大小是arr.length
const r = Memory.alloc(arr.length);
//將arr陣列寫入R地址中
Memory.writeByteArray(r,arr);
//輸出
console.log(hexdump(r, {
offset: 0,
length: arr.length,
header: true,
ansi: false
}));
});
}
setImmediate(frida_Memory,0);
輸出如下,
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 72 6f 79 73 75 65 roysue
1.6.7 讀取記憶體Memory.readByteArray
將一個指定地址的資料,代碼示例如下:
function frida_Memory() {
Java.perform(function () {
//定義需要寫入的位元組陣列 這個位元組陣列是字串"roysue"的十六進制
var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];
//申請一個新的記憶體空間 回傳指標 大小是arr.length
const r = Memory.alloc(arr.length);
//將arr陣列寫入R地址中
Memory.writeByteArray(r,arr);
//讀取r指標,長度是arr.length 也就是會列印上面一樣的值
var buffer = Memory.readByteArray(r, arr.length);
//輸出
console.log("Memory.readByteArray:");
console.log(hexdump(buffer, {
offset: 0,
length: arr.length,
header: true,
ansi: false
}));
});
});
}
setImmediate(frida_Memory,0);
輸出如下,
[Google Pixel::com.roysue.roysueapplication]-> Memory.readByteArray:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 72 6f 79 73 75 65 roysue
結語
在這篇中我們學會了在FRIDACLI中如何輸出想要輸出格式,也學會如何宣告變數,一步步的學習,在逐步的學習的程序,總是會遇到不同的問題,歌曲<奇跡再現>我相信你一定聽過吧~,新的風暴已經出現,怎么能夠停止不前..遇到問題不要怕,總會解決的,
短視頻、直播資料實時采集介面,請查看檔案: TiToData
免責宣告:本檔案僅供學習與參考,請勿用于非法用途!否則一切后果自負,
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/250121.html
標籤:其他
下一篇:redis-集群(1)
