一、內核模塊符號解析
1.1 內核模塊重定位函數調用
1.1.1 struct load_info info
SYSCALL_DEFINE3(init_module,......)
-->load_module()
-->simplify_symbols()
-->apply_relocations()
-->post_relocation()
加載模塊只需要讀入模塊的二進制代碼即可,然后執(zhí)行init_module系統(tǒng)調用。
我們先介紹下struct load_info info結構體。
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
......
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
return load_module(&info, uargs, 0);
}
struct load_info {
const char *name;
/* pointer to module in temporary copy, freed at end of load_module() */
struct module *mod;
Elf_Ehdr *hdr;
unsigned long len;
Elf_Shdr *sechdrs;
char *secstrings, *strtab;
unsigned long symoffs, stroffs;
struct _ddebug *debug;
unsigned int num_debug;
bool sig_ok;
#ifdef CONFIG_KALLSYMS
unsigned long mod_kallsyms_init_off;
#endif
struct {
unsigned int sym, str, mod, vers, info, pcpu;
} index;
};
struct load_info 是一個用于加載模塊時存儲相關信息的數據結構。
該結構體包含以下成員:
name:模塊的名稱,以字符串形式存儲。
mod:指向臨時復制的模塊結構體的指針,在 load_module() 結束時釋放。
hdr:指向 ELF 文件頭(Elf_Ehdr)的指針,用于存儲模塊的 ELF 文件頭信息。
len:模塊數據的長度(字節(jié)數)。
sechdrs:指向節(jié)頭表(Elf_Shdr)的指針,用于存儲模塊的節(jié)頭表信息。
secstrings:指向節(jié)頭字符串表的指針,用于存儲節(jié)頭字符串表的內容。
strtab:指向字符串表的指針,用于存儲字符串表的內容。
symoffs:符號表的偏移量。
stroffs:字符串表的偏移量。
debug:指向 _ddebug 結構體的指針,用于存儲調試信息。
num_debug:調試信息的數量。
sig_ok:表示模塊是否通過了數字簽名驗證。
mod_kallsyms_init_off:用于內核符號表中模塊的初始化偏移量。
index:一個匿名結構體,包含以下成員:
sym:符號表索引。
str:字符串表索引。
mod:模塊索引。
vers:版本索引。
info:信息索引。
pcpu:percpu 變量索引。
該結構體用于在加載模塊時存儲各種相關信息,包括模塊的名稱、ELF 文件頭、節(jié)頭表、字符串表、符號表等。這些信息在模塊加載和處理過程中起著重要的作用,用于解析和定位模塊的各個部分。
struct load_info 在模塊加載過程中起著重要作用。它作為一個容器,用于存儲與正在加載的模塊相關的各種信息和數據。struct load_info 的目的是在加載過程中促進模塊的解析、處理和初始化。以下是它的主要作用:
存儲模塊元數據:該結構體保存了關于模塊的基本元數據,例如模塊的名稱(name)。這些信息有助于識別和區(qū)分正在加載的模塊。
臨時模塊存儲:mod 成員指向模塊結構體(struct module)的臨時副本。這個副本在加載過程中使用,并在 load_module() 函數結束時釋放。它允許加載過程在模塊完全初始化之前對模塊結構進行操作和修改。
ELF 頭和節(jié)頭存儲:hdr 成員是指向模塊的 ELF 頭部(Elf_Ehdr)的指針。它存儲了關于 ELF 文件格式的信息,例如入口點和節(jié)頭表的位置。sechdrs 成員指向節(jié)頭表(Elf_Shdr),其中包含了關于模塊每個節(jié)的詳細信息。
字符串和符號表存儲:secstrings 成員保存了節(jié)頭字符串表,其中存儲了各個節(jié)的名稱。strtab 成員指向字符串表,它包含了模塊使用的各種字符串,如符號名稱。這些表對于符號解析和調試非常重要。
符號和字符串偏移量:symoffs 和 stroffs 成員分別存儲了符號表和字符串表在模塊中的偏移量。這些偏移量用于在符號解析和其他操作中定位和訪問符號和字符串。
調試信息:debug 成員指向 _ddebug 結構體,它存儲了模塊的調試信息。這些信息對于調試和跟蹤模塊的行為非常有用。
其他輔助信息:該結構體還包括其他成員,如 num_debug(調試條目的數量)、sig_ok(指示模塊是否通過了數字簽名驗證)、mod_kallsyms_init_off(模塊在內核符號表中的初始化偏移量)和 index(一個嵌套的結構體,包含不同類型表的各種索引)。
1.1.2 copy_module_from_user
/* Sets info->hdr and info->len. */
static int copy_module_from_user(const void __user *umod, unsigned long len,
struct load_info *info)
{
int err;
info->len = len;
if (info->len < sizeof(*(info->hdr)))
return -ENOEXEC;
err = security_kernel_load_data(LOADING_MODULE);
if (err)
return err;
/* Suck in entire file: we'll want most of it. */
info->hdr = __vmalloc(info->len,
GFP_KERNEL | __GFP_NOWARN, PAGE_KERNEL);
if (!info->hdr)
return -ENOMEM;
if (copy_chunked_from_user(info->hdr, umod, info->len) != 0) {
vfree(info->hdr);
return -EFAULT;
}
return 0;
}
這是一個用于從用戶空間復制模塊數據到內核空間的函數 copy_module_from_user 的代碼片段。該函數接受用戶空間的模塊數據 umod 和數據長度 len,并將其復制到內核空間中的 info->hdr 中。
函數的實現邏輯如下:
(1)首先,將數據長度 len 存儲到 info->len 中。
(2)如果數據長度小于 info->hdr 的大小,表示數據長度不足以存儲模塊的頭部信息,這種情況下返回錯誤碼 -ENOEXEC。
(3)調用 security_kernel_load_data 函數來進行內核安全性檢查。如果檢查失敗,返回相應的錯誤碼。
int security_kernel_load_data(enum kernel_load_data_id id)
{
int ret;
ret = call_int_hook(kernel_load_data, 0, id);
if (ret)
return ret;
return ima_load_data(id);
}
EXPORT_SYMBOL_GPL(security_kernel_load_data);
security_kernel_load_data是內核模塊加載過程中的一個 LSM點。
(4)分配內核可執(zhí)行的虛擬內存空間來存儲模塊的頭部信息。使用 __vmalloc 函數來分配內存,分配的大小為 info->len 字節(jié)。如果內存分配失敗,返回錯誤碼 -ENOMEM。
(5)使用 copy_chunked_from_user 函數將用戶空間的模塊數據 umod 復制到內核空間的 info->hdr 中。如果復制過程中出現錯誤,釋放之前分配的內存并返回錯誤碼 -EFAULT。
(6)如果復制成功,返回 0 表示成功。
該函數的主要功能是從用戶空間復制模塊數據到內核空間,并將復制后的數據存儲在 info->hdr 中供后續(xù)處理和加載使用。
static int copy_chunked_from_user(void *dst, const void __user *usrc, unsigned long len)
{
do {
unsigned long n = min(len, COPY_CHUNK_SIZE);
if (copy_from_user(dst, usrc, n) != 0)
return -EFAULT;
cond_resched();
dst += n;
usrc += n;
len -= n;
} while (len);
return 0;
}
這是一個用于從用戶空間按塊復制數據到內核空間的函數 copy_chunked_from_user 的代碼片段。該函數接受目標內核地址 dst、源用戶地址 usrc 和數據長度 len,并按照指定的塊大小進行分塊復制。
函數的實現邏輯如下:
進入一個循環(huán),直到全部數據被復制完畢。
在每次循環(huán)中,計算當前塊的大小,選擇較小值作為復制的長度。min(len, COPY_CHUNK_SIZE) 中的 COPY_CHUNK_SIZE 是一個預定義的常量,表示每個塊的大小。
使用 copy_from_user 函數將當前塊的數據從用戶空間復制到目標內核地址 dst。如果復制過程中出現錯誤,返回錯誤碼 -EFAULT。
調用 cond_resched 函數,讓出 CPU,允許其他任務執(zhí)行,以提高系統(tǒng)的響應性。
更新目標內核地址 dst、源用戶地址 usrc 和剩余長度 len,以便處理下一個塊。
檢查剩余長度 len 是否為 0,如果仍有剩余數據,則繼續(xù)循環(huán),否則退出循環(huán)。
如果數據復制成功,返回 0 表示成功。
該函數的作用是按照指定的塊大小,從用戶空間按塊復制數據到目標內核地址。由于復制過程中可能會耗費較長時間,因此在每個塊的復制結束后調用 cond_resched 函數以確保系統(tǒng)能夠及時響應其他任務。
1.2 simplify_symbols
1.2.1 simplify_symbols
/* Change all symbols so that st_value encodes the pointer directly. */
static int simplify_symbols(struct module *mod, const struct load_info *info)
{
Elf_Shdr *symsec = &info->sechdrs[info->index.sym];
Elf_Sym *sym = (void *)symsec->sh_addr;
unsigned long secbase;
unsigned int i;
int ret = 0;
const struct kernel_symbol *ksym;
for (i = 1; i < symsec->sh_size / sizeof(Elf_Sym); i++) {
const char *name = info->strtab + sym[i].st_name;
switch (sym[i].st_shndx) {
case SHN_COMMON:
/* Ignore common symbols */
if (!strncmp(name, "__gnu_lto", 9))
break;
/* We compiled with -fno-common. These are not
supposed to happen. */
pr_debug("Common symbol: %s\n", name);
pr_warn("%s: please compile with -fno-common\n",
mod->name);
ret = -ENOEXEC;
break;
case SHN_ABS:
/* Don't need to do anything */
pr_debug("Absolute symbol: 0x%08lx\n",
(long)sym[i].st_value);
break;
case SHN_LIVEPATCH:
/* Livepatch symbols are resolved by livepatch */
break;
case SHN_UNDEF:
ksym = resolve_symbol_wait(mod, info, name);
/* Ok if resolved. */
if (ksym && !IS_ERR(ksym)) {
sym[i].st_value = kernel_symbol_value(ksym);
break;
}
/* Ok if weak. */
if (!ksym && ELF_ST_BIND(sym[i].st_info) == STB_WEAK)
break;
ret = PTR_ERR(ksym) ?: -ENOENT;
pr_warn("%s: Unknown symbol %s (err %d)\n",
mod->name, name, ret);
break;
default:
/* Divert to percpu allocation if a percpu var. */
if (sym[i].st_shndx == info->index.pcpu)
secbase = (unsigned long)mod_percpu(mod);
else
secbase = info->sechdrs[sym[i].st_shndx].sh_addr;
sym[i].st_value += secbase;
break;
}
}
return ret;
}
simplify_symbols是在 Linux 內核中用于簡化符號表的函數。它的作用是修改符號表中的符號,使得 st_value 字段直接編碼為指針值,以提高符號訪問的效率。
(1)首先,通過 info 參數獲取符號表的節(jié)頭信息(symsec)和符號表的起始地址(sym)。
(2)在 for 循環(huán)中,遍歷符號表中的每個符號(從索引 1 開始,索引 0 通常是一個特殊符號)。
(3)對于每個符號,從字符串表中獲取符號的名稱。名稱存儲在字符串表中的偏移量(st_name)指定的位置。
(4)根據符號的 st_shndx 字段的值,執(zhí)行不同的操作。
這里只考慮 st_shndx 字段 = SHN_UNDEF 的情況:
SHN_UNDEF:表示未定義符號,需要解析其值。使用 resolve_symbol_wait 函數嘗試解析符號。
如果成功解析符號,則將 st_value 設置為解析到的內核符號的值。
如果解析失敗但是符號屬于弱符號(weak symbol),則忽略解析失敗,保留符號的原始值。
如果解析失敗且符號不是弱符號,則將返回錯誤代碼,并打印警告信息。
這段代碼的目的是優(yōu)化符號訪問的效率,通過直接編碼指針值到符號表中的 st_value 字段,避免了在運行時進行額外的計算。這對于內核符號的訪問和解析過程非常重要,因為它們在內核的各個部分被廣泛使用。
1.2.2 resolve_symbol_wait
static const struct kernel_symbol *
resolve_symbol_wait(struct module *mod,
const struct load_info *info,
const char *name)
{
const struct kernel_symbol *ksym;
char owner[MODULE_NAME_LEN];
if (wait_event_interruptible_timeout(module_wq,
!IS_ERR(ksym = resolve_symbol(mod, info, name, owner))
|| PTR_ERR(ksym) != -EBUSY,
30 * HZ) <= 0) {
pr_warn("%s: gave up waiting for init of module %s.\n",
mod->name, owner);
}
return ksym;
}
這段代碼的目的是解析內核符號 – 使用 resolve_symbol 函數嘗試解析符號。該函數根據給定的模塊、加載信息和符號名稱,查找并返回符號的內核表示。,并在解析完成前等待模塊的初始化。它使用 resolve_symbol 函數來實際解析符號,并使用 wait_event_interruptible_timeout 函數來等待模塊初始化完成。等待的目的是確保符號的解析是在模塊完全初始化之后進行的,以避免潛在的競爭條件或使用未完全初始化的模塊導致的錯誤。
1.2.3 resolve_symbol
/* Resolve a symbol for this module. I.e. if we find one, record usage. */
static const struct kernel_symbol *resolve_symbol(struct module *mod,
const struct load_info *info,
const char *name,
char ownername[])
{
struct module *owner;
const struct kernel_symbol *sym;
const s32 *crc;
int err;
/*
* The module_mutex should not be a heavily contended lock;
* if we get the occasional sleep here, we'll go an extra iteration
* in the wait_event_interruptible(), which is harmless.
*/
sched_annotate_sleep();
mutex_lock(&module_mutex);
sym = find_symbol(name, &owner, &crc,
!(mod->taints & (1 << TAINT_PROPRIETARY_MODULE)), true);
if (!sym)
goto unlock;
if (!check_version(info, name, mod, crc)) {
sym = ERR_PTR(-EINVAL);
goto getname;
}
err = ref_module(mod, owner);
if (err) {
sym = ERR_PTR(err);
goto getname;
}
getname:
/* We must make copy under the lock if we failed to get ref. */
strncpy(ownername, module_name(owner), MODULE_NAME_LEN);
unlock:
mutex_unlock(&module_mutex);
return sym;
}
函數 resolve_symbol,用于解析內核模塊中未定義的符號引用,并記錄符號的使用情況。
1.2.4 find_symbol
/* Find a symbol and return it, along with, (optional) crc and
* (optional) module which owns it. Needs preempt disabled or module_mutex. */
const struct kernel_symbol *find_symbol(const char *name,
struct module **owner,
const s32 **crc,
bool gplok,
bool warn)
{
struct find_symbol_arg fsa;
fsa.name = name;
fsa.gplok = gplok;
fsa.warn = warn;
if (each_symbol_section(find_symbol_in_section, &fsa)) {
if (owner)
*owner = fsa.owner;
if (crc)
*crc = fsa.crc;
return fsa.sym;
}
pr_debug("Failed to find symbol %s\n", name);
return NULL;
}
EXPORT_SYMBOL_GPL(find_symbol);
函數 find_symbol,用于在內核中查找符號并返回它,同時可選地返回符號的校驗和和擁有者模塊。
調用 each_symbol_section 函數,對每個符號節(jié)調用 find_symbol_in_section 函數進行符號查找。
如果找到符號,將符號指針賦值給 fsa.sym,并可選地將符號所屬的模塊賦值給 fsa.owner,將符號的校驗和賦值給 fsa.crc。
如果找到符號,返回 fsa.sym。
該函數用于在內核中查找給定名稱的符號。它通過遍歷每個符號節(jié)來查找符號,并在找到符號后返回相應的信息。函數的實現依賴于 each_symbol_section 和 find_symbol_in_section 函數。
二、 apply_relocations
2.1 apply_relocations
static int apply_relocations(struct module *mod, const struct load_info *info)
{
unsigned int i;
int err = 0;
/* Now do relocations. */
for (i = 1; i < info->hdr->e_shnum; i++) {
unsigned int infosec = info->sechdrs[i].sh_info;
/* Not a valid relocation section? */
if (infosec >= info->hdr->e_shnum)
continue;
/* Don't bother with non-allocated sections */
if (!(info->sechdrs[infosec].sh_flags & SHF_ALLOC))
continue;
/* Livepatch relocation sections are applied by livepatch */
if (info->sechdrs[i].sh_flags & SHF_RELA_LIVEPATCH)
continue;
if (info->sechdrs[i].sh_type == SHT_REL)
err = apply_relocate(info->sechdrs, info->strtab,
info->index.sym, i, mod);
else if (info->sechdrs[i].sh_type == SHT_RELA)
err = apply_relocate_add(info->sechdrs, info->strtab,
info->index.sym, i, mod);
if (err < 0)
break;
}
return err;
}
使用一個循環(huán)遍歷模塊加載信息中的每個重定位節(jié)。
獲取當前重定位節(jié)的 sh_info 字段,表示關聯的節(jié)索引。如果 sh_info 不是有效的節(jié)索引(大于等于節(jié)的總數),則跳過該重定位節(jié)。
檢查當前重定位節(jié)的 sh_flags 字段是否包含 SHF_ALLOC 標志,判斷是否為已分配的節(jié)。如果不是已分配的節(jié),則跳過該重定位節(jié)。
檢查當前重定位節(jié)的 sh_flags 字段是否包含 SHF_RELA_LIVEPATCH 標志,判斷是否為 Livepatch 重定位節(jié)。如果是 Livepatch 重定位節(jié),則跳過該重定位節(jié)。
根據重定位節(jié)的類型進行相應的重定位操作:
如果重定位節(jié)的類型是 SHT_REL,則調用 apply_relocate 函數應用重定位信息。
如果重定位節(jié)的類型是 SHT_RELA,則調用 apply_relocate_add 函數應用重定位信息。
對于內核模塊重定位的類型基本都是SHT_RELA,所以我們關注的是apply_relocate_add 函數。
2.1.1 Elf64_Ehdr
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
Elf64_Ehdr 是一個用于表示 ELF64(64位 ELF 文件格式)文件頭的結構體類型。它包含以下成員:
e_ident:ELF 文件的標識符數組,也稱為 “ELF 魔數”。它用于識別文件是否符合 ELF 文件格式的規(guī)范。
e_type:ELF 文件的類型。它表示文件的類型,如可執(zhí)行文件、共享對象、目標文件等。
e_machine:目標體系結構的架構類型。它表示文件的目標運行環(huán)境所使用的體系結構。
e_version:ELF 文件的版本號。它指示 ELF 文件格式的版本。
e_entry:程序的入口點的虛擬地址。它表示程序執(zhí)行的起始地址。
e_phoff:程序頭表(Program Header Table)在文件中的偏移量。它指示程序頭表的位置和大小。
e_shoff:節(jié)頭表(Section Header Table)在文件中的偏移量。它指示節(jié)頭表的位置和大小。
e_flags:特定標志位。它包含一些特定于體系結構或文件的標志。
e_ehsize:ELF 文件頭的大小。它表示 ELF 文件頭的字節(jié)大小。
e_phentsize:程序頭表項的大小。它表示每個程序頭表項的字節(jié)大小。
e_phnum:程序頭表中的條目數。它表示程序頭表中包含的程序頭表項的數量。
e_shentsize:節(jié)頭表項的大小。它表示每個節(jié)頭表項的字節(jié)大小。
e_shnum:節(jié)頭表中的條目數。它表示節(jié)頭表中包含的節(jié)頭表項的數量。
e_shstrndx:節(jié)頭字符串表的索引。它表示節(jié)頭字符串表在節(jié)頭表中的索引,用于查找節(jié)名。
對于內核模塊重定位過程中,對于Elf64_Ehdr我們需要關注的字段是e_shnum。
e_shnum:節(jié)頭表中的條目數。它表示節(jié)頭表中包含的節(jié)頭表項的數量。
2.1.2 Elf64_Shdr
typedef struct elf64_shdr {
Elf64_Word sh_name; /* Section name, index in string tbl */
Elf64_Word sh_type; /* Type of section */
Elf64_Xword sh_flags; /* Miscellaneous section attributes */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Size of section in bytes */
Elf64_Word sh_link; /* Index of another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
Elf64_Shdr 結構體定義了 ELF64 格式中節(jié)頭部的布局和字段的含義,具體解釋如下:
sh_name:該字段是一個索引,指示節(jié)的名稱在字符串表中的位置。字符串表是一個包含所有節(jié)名的字符串數組,它位于 ELF 文件的一個特殊節(jié)中。
sh_type:該字段指示節(jié)的類型,定義了節(jié)所包含數據或代碼的語義。例如,常見的節(jié)類型包括代碼段、數據段、符號表、字符串表等。
sh_flags:該字段包含了節(jié)的各種屬性和標志。這些標志可以指示節(jié)的可寫性、可執(zhí)行性、對齊要求等屬性。
sh_addr:該字段指示節(jié)在程序執(zhí)行時的虛擬地址。對于可執(zhí)行文件,這是節(jié)在內存中加載的地址。
sh_offset:該字段指示節(jié)在 ELF 文件中的偏移量,即該節(jié)的數據或代碼在文件中的位置。
sh_size:該字段指示節(jié)的大?。ㄒ宰止?jié)為單位),即該節(jié)所占用的文件空間大小。
sh_link:該字段是一個索引,指示該節(jié)相關聯的其他節(jié)的索引。具體關聯的節(jié)類型取決于 sh_type 字段的值。
sh_info:該字段包含附加的節(jié)信息。具體的含義取決于 sh_type 字段的值。
sh_addralign:該字段指示節(jié)的對齊要求,即節(jié)在內存中的起始地址需要滿足的對齊條件。通常,節(jié)的對齊要求是根據節(jié)的特性和用途來確定的。
sh_entsize:該字段指示節(jié)中每個表項的大小。僅在節(jié)類型為表格型(如符號表)時才具有意義,用于計算表中條目的數量。
其中sh_info字段:
sh_info 字段是 ELF64 節(jié)頭部中的一個字段,用于存儲與特定節(jié)相關的附加信息。該字段的具體含義取決于節(jié)的類型(sh_type 字段)。
對于某些特定的節(jié)類型,sh_info 字段具有以下含義:
(1)對于符號表節(jié)(SHT_SYMTAB)和動態(tài)符號表節(jié)(SHT_DYNSYM),sh_info 字段存儲了符號表中本地符號的數量。本地符號是指與當前目標文件或共享對象相關的符號。通過 sh_info 字段的值,可以確定符號表中本地符號的范圍。
(2)對于重定位節(jié)(SHT_REL 和 SHT_RELA),sh_info 字段指示關聯的節(jié)的索引。這個關聯節(jié)包含了重定位所需的目標符號或節(jié)的信息。通過 sh_info 字段的值,可以確定重定位節(jié)與其關聯的目標節(jié)。
對于這里我們主要關注 sh_type = SHT_RELA,通過 sh_info 字段的值,可以確定重定位節(jié)與其關聯的目標節(jié)。
假設有一個 ELF 文件,其中包含以下兩個節(jié):
.text 節(jié)(類型為 SHT_PROGBITS):包含可執(zhí)行代碼的指令。
.rel.text 節(jié)(類型為 SHT_RELA):包含與 .text 節(jié)相關的重定位信息。
在這個示例中,.rel.text 節(jié)是關聯于 .text 節(jié)的重定位節(jié)。sh_info 字段的值將提供有關如何解析 .rel.text 節(jié)的信息。
首先,查看 .rel.text 節(jié)的 sh_info 字段的值。假設 sh_info 字段的值為 4,這意味著 .rel.text 節(jié)與索引為 4 的節(jié)相關聯。
接下來,查找索引為 4 的節(jié),即關聯節(jié)。假設關聯節(jié)是 .symtab 節(jié)(符號表節(jié))。
通過這個例子,我們可以得出以下結論:
.rel.text 節(jié)與 .text 節(jié)相關聯,通過 sh_info 字段的值 4 來指示關聯節(jié)的索引為 4。
關聯節(jié)是 .symtab 節(jié),它可能包含了重定位所需的目標符號信息。
通過 sh_info 字段的值,可以確定關聯的節(jié),進而了解到與重定位相關的目標符號或目標節(jié)的位置和信息。
2.2 x86_64
int apply_relocate_add(Elf64_Shdr *sechdrs,
const char *strtab,
unsigned int symindex,
unsigned int relsec,
struct module *me)
{
unsigned int i;
Elf64_Rela *rel = (void *)sechdrs[relsec].sh_addr;
Elf64_Sym *sym;
void *loc;
u64 val;
for (i = 0; i < sechdrs[relsec].sh_size / sizeof(*rel); i++) {
/* This is where to make the change */
//獲取需要重定位的位置 P
loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr
+ rel[i].r_offset;
/* This is the symbol it is referring to. Note that all
undefined symbols have been resolved. */
//從符號表中獲取需要重定位符號的值 S
//符號表中有多個符號,我們需要獲取要重定位的符號,其在符號表中的位置索引:ELF64_R_SYM(rel[i].r_info)
sym = (Elf64_Sym *)sechdrs[symindex].sh_addr
+ ELF64_R_SYM(rel[i].r_info);
//獲取 S + A
val = sym->st_value + rel[i].r_addend;
switch (ELF64_R_TYPE(rel[i].r_info)) {
......
case R_X86_64_PC32:
case R_X86_64_PLT32:
if (*(u32 *)loc != 0)
goto invalid_relocation;
val -= (u64)loc;
*(u32 *)loc = val;
......
}
}
return 0;
......
}
該函數遍歷重定位節(jié)中的每個重定位項,并根據不同的重定位類型對指定位置進行修正。
其中 Elf64_Rela :
typedef struct elf64_rela {
Elf64_Addr r_offset; /* Location at which to apply the action */
Elf64_Xword r_info; /* index and type of relocation */
Elf64_Sxword r_addend; /* Constant addend used to compute value */
} Elf64_Rela;
Elf64_Rela 是一個用于表示 ELF64(64位 ELF 文件格式)重定位項的結構體類型。它包含以下成員:
(1)r_offset:重定位項的目標地址。它指示在哪個位置應用重定位操作。
(2)r_info:重定位項的索引和類型。索引和類型的組合指示了重定位操作應該如何執(zhí)行。具體來說,它將索引和重定位類型編碼為一個 64 位的無符號整數(Elf64_Xword 類型)。
(3)r_addend:用于計算修正值的常量加數。在執(zhí)行重定位操作時,將目標地址的值與該常量加數相加,以計算最終的修正值。
重定位項的索引和類型的計算:
用于從 64 位的 r_info 值中提取符號索引和重定位類型的宏定義。
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
ELF64_R_SYM(i) 宏將 64 位的 r_info 值右移 32 位,并返回高位部分,即符號索引。在 ELF64 格式中,符號索引占據了高 32 位。
ELF64_R_TYPE(i) 宏將 64 位的 r_info 值與 0xffffffff 進行按位與操作,返回低位部分,即重定位類型。在 ELF64 格式中,重定位類型占據了低 32 位。
這兩個宏的作用是從 r_info 值中提取符號索引和重定位類型,以便在重定位過程中進行符號解析和類型判斷。通過這樣的宏定義,可以方便地從 r_info 值中獲取所需的信息,而無需手動進行位操作。
其中 Elf64_Sym :
Elf64_Sym 是一個用于表示 ELF64(64位 ELF 文件格式)符號表項的結構體類型。它包含以下成員:
(1)st_name:符號的名稱在字符串表中的索引。它指示了符號的名稱在字符串表中的位置。
(2)st_info:符號的類型和綁定屬性。它是一個無符號字符,用于編碼符號的類型和綁定屬性。
(3)st_other:沒有定義的含義,通常為0。保留字段,未使用。
(4)st_shndx:關聯的節(jié)(section)索引。它表示符號所屬的節(jié)的索引,指示符號在哪個節(jié)中定義或引用。
(5)st_value:符號的值。它表示符號的地址或常量值。
(6)st_size:關聯的符號大小。它表示符號的大小或長度,以字節(jié)為單位。
Elf64_Rela *rel = (void *)sechdrs[relsec].sh_addr; 將重定位節(jié)的起始地址轉換為指向 Elf64_Rela 類型的指針,并將結果存儲在 rel 變量中。
loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr + rel[i].r_offset; 計算出當前重定位項指向的位置,根據重定位項的偏移量(r_offset)在指定節(jié)的起始地址上進行偏移。
sym = (Elf64_Sym *)sechdrs[symindex].sh_addr + ELF64_R_SYM(rel[i].r_info); 根據重定位項中的符號索引(ELF64_R_SYM(rel[i].r_info)),找到對應的符號表項,并將結果存儲在 sym 變量中。
val = sym->st_value + rel[i].r_addend; 計算出修正后的值,將符號的值(sym->st_value)與重定位項的附加值(rel[i].r_addend)相加,并將結果存儲在 val 變量中。
根據重定位項的類型進行不同的操作(這里我們只關注R_X86_64_PC32重定位類型):
case R_X86_64_PC32:
case R_X86_64_PLT32:
if (*(u32 *)loc != 0)
goto invalid_relocation;
val -= (u64)loc;
*(u32 *)loc = val;
對于 R_X86_64_PC32 和 R_X86_64_PLT32 類型,將修正后的值減去當前重定位項指向的位置,并將結果存儲到當前重定位項指向的位置。
val -= (u64)loc; 將修正后的值 val 減去當前重定位項指向的位置 loc,得到相對于當前位置的偏移量。
*(u32 *)loc = val; 將修正后的偏移量存儲到當前重定位項指向的位置。
這段代碼的作用是將修正后的相對偏移量(val)存儲到指定位置上,這些位置通常是指令中的相對跳轉或調用目標地址。在這種情況下,修正的偏移量是相對于當前指令的位置計算得出的。
2.3 Arm64
int apply_relocate_add(Elf64_Shdr *sechdrs,
const char *strtab,
unsigned int symindex,
unsigned int relsec,
struct module *me)
{
unsigned int i;
int ovf;
bool overflow_check;
Elf64_Sym *sym;
void *loc;
u64 val;
//獲取重定位節(jié)的起始虛擬地址
Elf64_Rela *rel = (void *)sechdrs[relsec].sh_addr;
for (i = 0; i < sechdrs[relsec].sh_size / sizeof(*rel); i++) {
//loc 對應于 AArch64 ELF 文檔中的 P
//通過 sh_info 字段的值,可以確定關聯的節(jié),獲取重定位節(jié)的虛擬地址
//重定位節(jié)的虛擬地址 + rel[i].r_offset = 需要重定位的位置 P
loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr
+ rel[i].r_offset;
/* sym is the ELF symbol we're referring to. */
//獲取該重定位項在符號表中的索引位置的符號項
//將符號表的起始地址轉化為 (Elf64_Sym *)指針,那么+一個索引值r_info,就表示獲取符號表中第r_info位置的符號項
sym = (Elf64_Sym *)sechdrs[symindex].sh_addr
+ ELF64_R_SYM(rel[i].r_info);
// val 對應于 AArch64 ELF 文檔中的 (S + A)
val = sym->st_value + rel[i].r_addend;
/* Check for overflow by default. */
overflow_check = true;
/* Perform the static relocation. */
switch (ELF64_R_TYPE(rel[i].r_info)) {
/* Null relocations. */
case R_ARM_NONE:
case R_AARCH64_NONE:
ovf = 0;
break;
/* Data relocations. */
......
case R_AARCH64_PREL32:
ovf = reloc_data(RELOC_OP_PREL, loc, val, 32);
break;
......
}
}
}
apply_relocate_add的函數,它對一個ELF(可執(zhí)行和可鏈接格式)內核模塊二進制文件進行重定位。重定位是調整二進制文件中符號引用的過程,使其在加載到內存時指向正確的內存位置。
函數通過循環(huán)迭代每個重定位節(jié)的重定位項來進行處理。在循環(huán)內部,根據重定位項的信息計算重定位符號的位置 loc 和 重定位符號的值val 。
然后,根據重定位類型 ELF64_R_TYPE(rel[i].r_info) 執(zhí)行特定類型的重定位。
根據重定位類型采取不同的處理方式,并相應地調用不同的輔助函數。
我們這里主要關注重定位類型 R_AARCH64_PREL32 。R_AARCH64_PREL32 對應的輔助函數reloc_data:
int ovf;
ovf = reloc_data(RELOC_OP_PREL, loc, val, 32);
enum aarch64_reloc_op {
RELOC_OP_NONE,
RELOC_OP_ABS,
RELOC_OP_PREL,
RELOC_OP_PAGE,
};
static u64 do_reloc(enum aarch64_reloc_op reloc_op, __le32 *place, u64 val)
{
switch (reloc_op) {
......
case RELOC_OP_PREL:
return val - (u64)place;
return 0;
......
}
pr_err("do_reloc: unknown relocation operation %d\n", reloc_op);
return 0;
}
static int reloc_data(enum aarch64_reloc_op op, void *place, u64 val, int len)
{
//調用 do_reloc 函數對傳入的值進行修正,并將修正后的值存儲在 sval 變量中。
s64 sval = do_reloc(op, place, val);
switch (len) {
......
//如果長度為 32,將 sval 強制轉換為 s32 類型,并將其存儲到 place 指針指向的位置上。
case 32:
*(s32 *)place = sval;
if (sval < S32_MIN || sval > U32_MAX)
return -ERANGE;
break;
......
}
return 0;
}
根據傳入的操作類型和值,將修正后的數據存儲到給定的位置指針處。即將重定位操作后修正后的數據寫到 loc(待重定位的位置)。
修正后的數據 :
result = val - loc
result = S + A - P
S = sym->st_value
A = rel[i].r_addend
p = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr + rel[i].r_offset;
(1)其中:
void *loc;
loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr + rel[i].r_offset;
sechdrs[relsec].sh_info表示重定位條目所屬的節(jié)的索引。
sechdrs[sechdrs[relsec].sh_info].sh_addr表示該節(jié)的起始地址。
(void *)表示將起始地址轉換為void *類型的指針,以便與偏移量進行相加。
rel[i].r_offset表示重定位條目中記錄的偏移量。
loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr + rel[i].r_offset;將起始地址與偏移量相加,得到重定位條目在內存中的絕對地址。
通過將計算得到的絕對地址賦值給loc,后續(xù)代碼可以使用loc來引用該重定位條目在內存中的位置。
loc 對應于 AArch64 ELF 文檔中的 P。
(2)其中:
Elf64_Sym *sym;
sym = (Elf64_Sym *)sechdrs[symindex].sh_addr + ELF64_R_SYM(rel[i].r_info);
是將符號表的起始地址(sechdrs[symindex].sh_addr)與重定位條目中的符號索引(ELF64_R_SYM(rel[i].r_info))相加,并將結果賦值給sym變量。
將符號表的起始地址轉化為 (Elf64_Sym *)指針,那么加上一個索引值r_info,就表示獲取符號表中第r_info位置的符號項。
這里是指針的特性,比如符號表是一個數組a,每一個數組成員都是Elf64_Sym類型,那么上述sym的值就等于 = a[rel[i].r_info]。獲取符號表數組第rel[i].r_info個元素,元素類型是Elf64_Sym。
具體解釋如下:
sechdrs[symindex].sh_addr表示指向符號表節(jié)的起始地址。
(Elf64_Sym *)表示將符號表的起始地址轉換為指向Elf64_Sym類型的指針。
ELF64_R_SYM(rel[i].r_info)從重定位條目的r_info字段中提取出符號索引。
sym = (Elf64_Sym *)sechdrs[symindex].sh_addr + ELF64_R_SYM(rel[i].r_info);將符號索引與符號表的起始地址相加,得到對應符號的地址,并將結果賦值給sym。
通過這行代碼,將符號的地址存儲在sym變量中,后續(xù)代碼可以使用sym來引用該符號的屬性,例如sym->st_value表示符號的值。
val = sym->st_value + rel[i].r_addend;
這行代碼的作用是計算出重定位后的值(val)。它將符號的值(sym->st_value)與重定位條目的附加值(rel[i].r_addend)相加,并將結果賦值給val變量。
具體解釋如下:
sym->st_value表示符號的值,即符號在內存中的地址或相對地址。
rel[i].r_addend表示重定位條目的附加值,用于修正符號的值。
val = sym->st_value + rel[i].r_addend將符號的值與重定位條目的附加值相加,得到修正后的值,并將結果賦值給val。
val 對應于 AArch64 ELF 文檔中的 (S + A)。文章來源:http://www.zghlxwxcb.cn/news/detail-683843.html
參考資料
Linux 4.19.90文章來源地址http://www.zghlxwxcb.cn/news/detail-683843.html
到了這里,關于Linux 內核模塊加載過程之重定位的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!