BPF核心函式(kfuncs)¶
1. 簡介¶
BPF核心函式,更常被稱為kfuncs,是Linux核心中暴露給BPF程式使用的函式。與普通的BPF輔助函式不同,kfuncs沒有穩定的介面,並且可能在一個核心版本到另一個核心版本之間發生變化。因此,BPF程式需要根據核心中的更改進行更新。有關更多資訊,請參見3. kfunc生命週期預期。
2. 定義kfunc¶
有兩種方法可以將核心函式暴露給BPF程式,一種是使核心中現有的函式可見,另一種是為BPF新增新的包裝器。在這兩種情況下,都必須注意BPF程式只能在有效的上下文中呼叫此類函式。為了強制執行此操作,kfunc的可見性可以是每個程式型別。
如果您沒有為現有的核心函式建立BPF包裝器,請跳到2.3 使用現有的核心函式。
2.1 建立包裝器kfunc¶
在定義包裝器kfunc時,包裝器函式應具有外部連結。這可以防止編譯器最佳化掉死程式碼,因為此包裝器kfunc不會在核心本身中被呼叫。沒有必要在標頭檔案中為包裝器kfunc提供原型。
下面給出一個示例
/* Disables missing prototype warnings */
__bpf_kfunc_start_defs();
__bpf_kfunc struct task_struct *bpf_find_get_task_by_vpid(pid_t nr)
{
return find_get_task_by_vpid(nr);
}
__bpf_kfunc_end_defs();
當我們需要註釋kfunc的引數時,通常需要包裝器kfunc。否則,可以透過向BPF子系統註冊來直接使kfunc對BPF程式可見。請參見2.3 使用現有的核心函式。
2.2 註釋kfunc引數¶
與BPF輔助函式類似,有時需要驗證器所需的其他上下文,以使核心函式的使用更安全,更有用。因此,我們可以透過在kfunc的引數名稱後加上__tag來註釋引數,其中tag可以是受支援的註釋之一。
2.2.1 __sz註釋¶
此註釋用於指示引數列表中的記憶體和大小對。下面給出一個示例
__bpf_kfunc void bpf_memzero(void *mem, int mem__sz)
{
...
}
在這裡,驗證器會將第一個引數視為PTR_TO_MEM,將第二個引數視為其大小。預設情況下,如果沒有__sz註釋,則使用指標的型別的大小。如果沒有__sz註釋,則kfunc不能接受void指標。
2.2.2 __k註釋¶
此註釋僅適用於標量引數,它表示驗證器必須檢查標量引數是否為已知常量,該常量不表示大小引數,並且常量的值與程式的安全性相關。
下面給出一個示例
__bpf_kfunc void *bpf_obj_new(u32 local_type_id__k, ...)
{
...
}
在這裡,bpf_obj_new使用local_type_id引數來查詢程式BTF中該型別ID的大小,並返回指向它的大小指標。每個型別ID將具有不同的大小,因此至關重要的是,在驗證器狀態修剪檢查期間值不匹配時,將每個此類呼叫視為不同的呼叫。
因此,只要kfunc接受的常量標量引數不是大小引數,並且常量的值對於程式安全性很重要,則應使用__k字尾。
2.2.3 __uninit註釋¶
此註釋用於指示該引數將被視為未初始化。
下面給出一個示例
__bpf_kfunc int bpf_dynptr_from_skb(..., struct bpf_dynptr_kern *ptr__uninit)
{
...
}
在這裡,dynptr將被視為未初始化的dynptr。如果沒有此註釋,如果傳入的dynptr未初始化,驗證器將拒絕該程式。
2.2.4 __opt註釋¶
此註釋用於指示與__sz或__szk引數關聯的緩衝區可能為空。如果該函式被傳遞了nullptr來代替緩衝區,則驗證器將不會檢查該長度是否適合該緩衝區。kfunc負責在使用此緩衝區之前檢查它是否為空。
下面給出一個示例
__bpf_kfunc void *bpf_dynptr_slice(..., void *buffer__opt, u32 buffer__szk)
{
...
}
在這裡,緩衝區可能為空。如果緩衝區不為空,則它至少是buffer_szk的大小。無論哪種方式,返回的緩衝區要麼是NULL,要麼是buffer_szk的大小。如果沒有此註釋,如果傳入一個非零大小的空指標,驗證器將拒絕該程式。
2.2.5 __str註釋¶
此註釋用於指示該引數是常量字串。
下面給出一個示例
__bpf_kfunc bpf_get_file_xattr(..., const char *name__str, ...)
{
...
}
在這種情況下,可以像這樣呼叫bpf_get_file_xattr()
bpf_get_file_xattr(..., "xattr_name", ...);
或者
const char name[] = "xattr_name"; /* This need to be global */
int BPF_PROG(...)
{
...
bpf_get_file_xattr(..., name, ...);
...
}
2.2.6 __prog註釋¶
此註釋用於指示需要將該引數修復為呼叫方BPF程式的bpf_prog_aux。傳遞給此引數的任何值都將被忽略,並由驗證器重寫。
下面給出一個示例
__bpf_kfunc int bpf_wq_set_callback_impl(struct bpf_wq *wq,
int (callback_fn)(void *map, int *key, void *value),
unsigned int flags,
void *aux__prog)
{
struct bpf_prog_aux *aux = aux__prog;
...
}
2.3 使用現有的核心函式¶
當核心中的現有函式適合由BPF程式使用時,可以直接將其註冊到BPF子系統。但是,仍然必須注意檢視BPF程式將呼叫它的上下文,以及這樣做是否安全。
2.4 註釋kfuncs¶
除了kfuncs的引數之外,驗證器可能還需要有關注冊到BPF子系統的kfunc型別的更多資訊。為此,我們在一組kfuncs上定義標誌,如下所示
BTF_KFUNCS_START(bpf_task_set)
BTF_ID_FLAGS(func, bpf_get_task_pid, KF_ACQUIRE | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_put_pid, KF_RELEASE)
BTF_KFUNCS_END(bpf_task_set)
此集合編碼了上面列出的每個kfunc的BTF ID,並將標誌與其一起編碼。當然,也可以指定不帶標誌。
kfunc定義也應始終用__bpf_kfunc宏進行註釋。這可以防止編譯器內聯kfunc(如果它是靜態核心函式)或該函式在LTO構建中被省略(因為它沒有在核心的其餘部分中使用)之類的問題。開發人員不應手動將註釋新增到其kfunc以防止這些問題。如果需要註釋來防止此類kfunc出現問題,則這是一個錯誤,應將其新增到宏定義中,以便其他kfuncs受到類似的保護。下面給出一個示例
__bpf_kfunc struct task_struct *bpf_get_task_pid(s32 pid)
{
...
}
2.4.1 KF_ACQUIRE標誌¶
KF_ACQUIRE標誌用於指示kfunc返回指向引用計數物件的指標。然後,驗證器將確保使用release kfunc最終釋放指向該物件的指標,或透過呼叫bpf_kptr_xchg將其傳輸到對映中使用引用kptr。否則,驗證器將無法載入BPF程式,直到程式的所有可能探索狀態中都沒有殘留的引用為止。
2.4.2 KF_RET_NULL標誌¶
KF_RET_NULL標誌用於指示kfunc返回的指標可能為NULL。因此,它強制使用者在使用(取消引用或傳遞給另一個輔助函式)從kfunc返回的指標之前,對該指標執行NULL檢查。此標誌通常與KF_ACQUIRE標誌配對使用,但兩者彼此正交。
2.4.3 KF_RELEASE標誌¶
KF_RELEASE標誌用於指示kfunc釋放傳遞給它的指標。只能傳遞一個被引用的指標。透過使用此標誌呼叫kfunc,釋放指標的所有副本都將失效。KF_RELEASE kfuncs會自動獲得下面描述的KF_TRUSTED_ARGS標誌提供的保護。
2.4.4 KF_TRUSTED_ARGS標誌¶
KF_TRUSTED_ARGS標誌用於採用指標引數的kfuncs。它指示所有指標引數都是有效的,並且所有指向BTF物件的指標都已以其未修改的形式傳遞(即,零偏移量,並且沒有從遍歷另一個指標獲得,下面描述的一個例外)。
有兩種型別的指向核心物件的指標被認為是“有效”的
作為跟蹤點或struct_ops回撥引數傳遞的指標。
從KF_ACQUIRE kfunc返回的指標。
指向非BTF物件的指標(例如標量指標)也可以傳遞給KF_TRUSTED_ARGS kfuncs,並且可以具有非零偏移量。
“有效”指標的定義隨時可能更改,並且絕對沒有任何ABI穩定性保證。
如上所述,從遍歷受信任指標獲得的巢狀指標不再受信任,但有一個例外。如果結構型別具有一個欄位,只要其父指標有效,就可以保證該欄位有效(受信任或rcu,如KF_RCU描述中所述),則可以使用以下宏向驗證器表達這一點
BTF_TYPE_SAFE_TRUSTEDBTF_TYPE_SAFE_RCUBTF_TYPE_SAFE_RCU_OR_NULL
例如,
BTF_TYPE_SAFE_TRUSTED(struct socket) {
struct sock *sk;
};
或
BTF_TYPE_SAFE_RCU(struct task_struct) {
const cpumask_t *cpus_ptr;
struct css_set __rcu *cgroups;
struct task_struct __rcu *real_parent;
struct task_struct *group_leader;
};
換句話說,你必須
將有效的指標型別包裝在
BTF_TYPE_SAFE_*宏中。指定有效巢狀欄位的型別和名稱。此欄位必須與原始型別定義中的欄位完全匹配。
BTF_TYPE_SAFE_*宏宣告的新型別也需要發出,以便它出現在BTF中。例如,BTF_TYPE_SAFE_TRUSTED(struct socket)在type_is_trusted()函式中發出,如下所示
BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED(struct socket));
2.4.5 KF_SLEEPABLE標誌¶
KF_SLEEPABLE標誌用於可能休眠的kfuncs。此類kfuncs只能由可休眠的BPF程式(BPF_F_SLEEPABLE)呼叫。
2.4.6 KF_DESTRUCTIVE標誌¶
KF_DESTRUCTIVE標誌用於指示呼叫哪些函式會對系統造成破壞。例如,這樣的呼叫可能會導致系統重新啟動或崩潰。由於此原因,這些呼叫適用其他限制。目前,它們僅需要CAP_SYS_BOOT功能,但以後可以新增更多功能。
2.4.7 KF_RCU標誌¶
KF_RCU標誌是KF_TRUSTED_ARGS的較弱版本。標有KF_RCU的kfuncs期望PTR_TRUSTED或MEM_RCU引數。驗證器保證物件有效,並且不存在use-after-free。指標不為NULL,但是物件的引用計數可能已達到零。kfuncs需要考慮執行refcnt != 0檢查,尤其是在返回KF_ACQUIRE指標時。還要注意,KF_ACQUIRE kfunc為KF_RCU時,很可能也應該為KF_RET_NULL。
2.4.8 KF_DEPRECATED標誌¶
KF_DEPRECATED標誌用於計劃在後續核心版本中更改或刪除的kfuncs。標有KF_DEPRECATED的kfunc也應在其核心文件中捕獲任何相關資訊。此類資訊通常包括kfunc的預期剩餘壽命,對可以替換它的新功能的建議(如果有),以及刪除它的理由。
請注意,儘管在某些情況下,可能會繼續支援KF_DEPRECATED kfunc並刪除其KF_DEPRECATED標誌,但在新增KF_DEPRECATED標誌之後刪除它可能比一開始就阻止新增它要困難得多。如3. kfunc生命週期預期中所述,鼓勵依賴特定kfuncs的使用者儘早瞭解其用例,並參與有關是否保留、更改、棄用或刪除這些kfuncs的上游討論(如果並且在發生此類討論時)。
2.5 註冊kfuncs¶
一旦kfunc準備好使用,使其可見的最後一步是將其註冊到BPF子系統。註冊是按BPF程式型別完成的。下面顯示一個示例
BTF_KFUNCS_START(bpf_task_set)
BTF_ID_FLAGS(func, bpf_get_task_pid, KF_ACQUIRE | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_put_pid, KF_RELEASE)
BTF_KFUNCS_END(bpf_task_set)
static const struct btf_kfunc_id_set bpf_task_kfunc_set = {
.owner = THIS_MODULE,
.set = &bpf_task_set,
};
static int init_subsystem(void)
{
return register_btf_kfunc_id_set(BPF_PROG_TYPE_TRACING, &bpf_task_kfunc_set);
}
late_initcall(init_subsystem);
2.6 使用___init指定no-cast別名¶
驗證器將始終強制執行由BPF程式傳遞給kfunc的指標的BTF型別與kfunc定義中指定的指標型別匹配。但是,即使它們的BTF_IDs不同,驗證器也允許根據C標準被認為是等效的型別傳遞給同一個kfunc引數。
例如,對於以下型別定義
struct bpf_cpumask {
cpumask_t cpumask;
refcount_t usage;
};
驗證器將允許將struct bpf_cpumask *傳遞給採用cpumask_t *的kfunc(它是struct cpumask *的typedef)。例如,struct cpumask *和struct bpf_cpmuask *都可以傳遞給bpf_cpumask_test_cpu()。
在某些情況下,不希望使用此類型別名行為。struct nf_conn___init就是一個這樣的例子
struct nf_conn___init {
struct nf_conn ct;
};
C標準會將這些型別視為等效的,但將任一型別傳遞給受信任的kfunc並不總是安全的。struct nf_conn___init表示已分配的struct nf_conn物件,該物件尚未初始化,因此將struct nf_conn___init *傳遞給期望完全初始化的struct nf_conn *(例如bpf_ct_change_timeout())的kfunc是不安全的。
為了滿足這些要求,如果兩種型別具有完全相同的名稱,並且其中一種型別的字尾為___init,則驗證器將強制執行嚴格的PTR_TO_BTF_ID型別匹配。
3. kfunc生命週期預期¶
kfuncs提供了一個核心 <-> 核心 API,因此不受與核心 <-> 使用者 UAPI 關聯的任何嚴格的穩定性限制的約束。這意味著可以將它們視為類似於 EXPORT_SYMBOL_GPL,因此可以由定義它們的子系統的維護人員在認為必要時修改或刪除。
與對核心的任何其他更改一樣,維護人員不會在沒有合理的理由的情況下更改或刪除 kfunc。他們是否選擇更改 kfunc 最終取決於各種因素,例如 kfunc 的使用範圍、kfunc 在核心中的時間、是否存在替代 kfunc、相關子系統的穩定性規範,以及當然繼續支援 kfunc 的技術成本。
這有幾個含義
被廣泛使用或在核心中存在很長時間的 kfuncs 將更難以證明維護人員可以更改或刪除。換句話說,已知有很多使用者並且提供重要價值的 kfuncs 為維護人員投入時間和複雜性來支援它們提供了更強的動力。因此,在其 BPF 程式中使用 kfuncs 的開發人員溝通和解釋如何以及為什麼要使用這些 kfuncs,並在上游討論發生時參與有關這些 kfuncs 的討論非常重要。
與標有 EXPORT_SYMBOL_GPL 的常規核心符號不同,呼叫 kfuncs 的 BPF 程式通常不是核心樹的一部分。這意味著當 kfunc 更改時,重構通常無法就地更改呼叫者,就像當核心符號更改時就地更新上游驅動程式一樣。
與常規核心符號不同,這是 BPF 符號的預期行為,並且使用 kfuncs 的樹外 BPF 程式應被認為與修改和刪除這些 kfuncs 的討論和決策相關。BPF 社群將在必要時積極參與上游討論,以確保考慮到此類使用者的觀點。
kfunc 永遠不會有任何硬性穩定性保證。BPF API 不能也不會僅僅出於穩定性原因而硬性阻止核心中的更改。也就是說,kfuncs 是旨在解決問題併為使用者提供價值的功能。是否更改或刪除 kfunc 的決定是一個多變數技術決策,該決策是根據具體情況做出的,並且由上述資料點等資訊告知。預計在沒有警告的情況下刪除或更改 kfunc 不會是一個常見的事件或在沒有充分理由的情況下發生,但是如果一個人要使用 kfuncs,則必須接受這種可能性。
3.1 kfunc 棄用¶
如上所述,雖然有時維護人員可能會發現必須立即更改或刪除 kfunc 以適應其子系統中的某些更改,但通常 kfuncs 將能夠適應更長和更謹慎的棄用過程。例如,如果出現一個新的 kfunc,它為現有的 kfunc 提供了卓越的功能,則現有的 kfunc 可能會被棄用一段時間,以允許使用者遷移其 BPF 程式以使用新的 kfunc。或者,如果 kfunc 沒有已知使用者,則可能會決定在某個棄用期後刪除該 kfunc(不提供替代 API),以便為使用者提供一個視窗來通知 kfunc 維護人員,如果事實證明 kfunc 實際上正在使用。
預計常見的情況是 kfuncs 將經歷一個棄用期,而不是在沒有警告的情況下被更改或刪除。如2.4.8 KF_DEPRECATED標誌中所述,kfunc 框架為 kfunc 開發人員提供了 KF_DEPRECATED 標誌,以向用戶發出 kfunc 已被棄用的訊號。一旦 kfunc 標有 KF_DEPRECATED,則移除過程如下
棄用的 kfuncs 的任何相關資訊都記錄在 kfunc 的核心文件中。此文件通常包括 kfunc 的預期剩餘壽命,對可以替換棄用函式的新功能的建議(或解釋為什麼不存在此類替換),等等。
棄用的 kfunc 在首次標記為棄用後會在核心中保留一段時間。此時間段將根據具體情況選擇,並且通常取決於 kfunc 的使用範圍、它在核心中的時間以及遷移到替代方案的難度。此棄用時間段是“盡最大努力”,並且如above中所述,有時情況可能表明必須在完整的預期棄用期過去之前刪除 kfunc。
在棄用期之後,kfunc 將被刪除。此時,呼叫 kfunc 的 BPF 程式將被驗證器拒絕。
4. 核心 kfuncs¶
BPF 子系統提供了許多“核心” kfuncs,這些 kfuncs 可能適用於各種不同的可能用例和程式。這些 kfuncs 在此處記錄。
4.1 struct task_struct * kfuncs¶
有許多 kfuncs 允許將struct task_struct *物件用作 kptrs
-
__bpf_kfunc struct task_struct *bpf_task_acquire(struct task_struct *p)¶
獲取對任務的引用。由此 kfunc 獲取的任務如果未作為 kptr 儲存在對映中,則必須透過呼叫
bpf_task_release()釋放。
引數
struct task_struct *p正在獲取引用的任務。
-
__bpf_kfunc void bpf_task_release(struct task_struct *p)¶
釋放在任務上獲取的引用。
引數
struct task_struct *p正在釋放引用的任務。
當您想要獲取或釋放在struct task_struct *上獲取的引用(例如作為跟蹤點引數或 struct_ops 回撥引數傳遞)時,這些 kfuncs 很有用。例如
/**
* A trivial example tracepoint program that shows how to
* acquire and release a struct task_struct * pointer.
*/
SEC("tp_btf/task_newtask")
int BPF_PROG(task_acquire_release_example, struct task_struct *task, u64 clone_flags)
{
struct task_struct *acquired;
acquired = bpf_task_acquire(task);
if (acquired)
/*
* In a typical program you'd do something like store
* the task in a map, and the map will automatically
* release it later. Here, we release it manually.
*/
bpf_task_release(acquired);
return 0;
}
在struct task_struct *物件上獲取的引用受 RCU 保護。因此,當在 RCU 讀取區域中時,您可以獲得指向嵌入在對映值中的任務的指標,而無需獲取引用
#define private(name) SEC(".data." #name) __hidden __attribute__((aligned(8)))
private(TASK) static struct task_struct *global;
/**
* A trivial example showing how to access a task stored
* in a map using RCU.
*/
SEC("tp_btf/task_newtask")
int BPF_PROG(task_rcu_read_example, struct task_struct *task, u64 clone_flags)
{
struct task_struct *local_copy;
bpf_rcu_read_lock();
local_copy = global;
if (local_copy)
/*
* We could also pass local_copy to kfuncs or helper functions here,
* as we're guaranteed that local_copy will be valid until we exit
* the RCU read region below.
*/
bpf_printk("Global task %s is valid", local_copy->comm);
else
bpf_printk("No global task found");
bpf_rcu_read_unlock();
/* At this point we can no longer reference local_copy. */
return 0;
}
BPF 程式還可以從 pid 查詢任務。如果呼叫方沒有指向struct task_struct *物件的受信任指標,可以使用bpf_task_acquire()獲取引用,這可能很有用。
-
__bpf_kfunc struct task_struct *bpf_task_from_pid(s32 pid)¶
透過在根 pid 名稱空間 idr 中查詢,從其 pid 查詢 struct task_struct。如果返回任務,則必須將其儲存在對映中,或者使用
bpf_task_release()釋放。
引數
s32 pid正在查詢的任務的 pid。
這是一個使用它的示例
SEC("tp_btf/task_newtask")
int BPF_PROG(task_get_pid_example, struct task_struct *task, u64 clone_flags)
{
struct task_struct *lookup;
lookup = bpf_task_from_pid(task->pid);
if (!lookup)
/* A task should always be found, as %task is a tracepoint arg. */
return -ENOENT;
if (lookup->pid != task->pid) {
/* bpf_task_from_pid() looks up the task via its
* globally-unique pid from the init_pid_ns. Thus,
* the pid of the lookup task should always be the
* same as the input task.
*/
bpf_task_release(lookup);
return -EINVAL;
}
/* bpf_task_from_pid() returns an acquired reference,
* so it must be dropped before returning from the
* tracepoint handler.
*/
bpf_task_release(lookup);
return 0;
}
4.2 struct cgroup * kfuncs¶
struct cgroup *物件也有獲取和釋放函式
-
__bpf_kfunc struct cgroup *bpf_cgroup_acquire(struct cgroup *cgrp)¶
獲取對 cgroup 的引用。由此 kfunc 獲取的 cgroup 如果未作為 kptr 儲存在對映中,則必須透過呼叫
bpf_cgroup_release()釋放。
引數
struct cgroup *cgrp正在獲取引用的 cgroup。
-
__bpf_kfunc void bpf_cgroup_release(struct cgroup *cgrp)¶
釋放在 cgroup 上獲取的引用。如果在 RCU 讀取區域中呼叫此 kfunc,則即使其引用計數降至 0,也保證 cgroup 在當前寬限期結束之前不會被釋放。
引數
struct cgroup *cgrp正在釋放引用的 cgroup。
這些 kfuncs 的使用方式與bpf_task_acquire()和bpf_task_release()分別完全相同,因此我們不會為它們提供示例。
其他可用於與struct cgroup *物件互動的 kfuncs 是bpf_cgroup_ancestor()和bpf_cgroup_from_id(),分別允許呼叫方訪問 cgroup 的祖先並按其 ID 查詢 cgroup。兩者都返回 cgroup kptr。
-
__bpf_kfunc struct cgroup *bpf_cgroup_ancestor(struct cgroup *cgrp, int level)¶
對 cgroup 的祖先陣列中的條目執行查詢。由此 kfunc 返回的 cgroup 如果未隨後儲存在對映中,則必須透過呼叫
bpf_cgroup_release()釋放。
引數
struct cgroup *cgrp我們正在為其執行查詢的 cgroup。
int level要查詢的祖先級別。
-
__bpf_kfunc struct cgroup *bpf_cgroup_from_id(u64 cgid)¶
透過 ID 查詢 cgroup。此 kfunc 返回的 cgroup 如果沒有隨後儲存在 map 中,則必須透過呼叫
bpf_cgroup_release()來釋放。
引數
u64 cgidcgroup ID。
最終,應該更新 BPF 以允許透過程式本身中的普通記憶體載入來完成此操作。如果沒有在驗證器中做更多的工作,目前這是不可能的。 bpf_cgroup_ancestor() 可以如下使用
/**
* Simple tracepoint example that illustrates how a cgroup's
* ancestor can be accessed using bpf_cgroup_ancestor().
*/
SEC("tp_btf/cgroup_mkdir")
int BPF_PROG(cgrp_ancestor_example, struct cgroup *cgrp, const char *path)
{
struct cgroup *parent;
/* The parent cgroup resides at the level before the current cgroup's level. */
parent = bpf_cgroup_ancestor(cgrp, cgrp->level - 1);
if (!parent)
return -ENOENT;
bpf_printk("Parent id is %d", parent->self.id);
/* Return the parent cgroup that was acquired above. */
bpf_cgroup_release(parent);
return 0;
}
4.3 struct cpumask * kfuncs¶
BPF 提供了一組 kfuncs,可以用於查詢、分配、修改和銷燬 struct cpumask * 物件。 請參閱 BPF cpumask kfuncs 瞭解更多詳細資訊。