国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Linux 內核模塊加載過程之重定位

這篇具有很好參考價值的文章主要介紹了Linux 內核模塊加載過程之重定位。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

一、內核模塊符號解析

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)。

參考資料

Linux 4.19.90文章來源地址http://www.zghlxwxcb.cn/news/detail-683843.html

到了這里,關于Linux 內核模塊加載過程之重定位的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉載,請注明出處: 如若內容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • Linux內核學習(十三)—— 設備與模塊(基于Linux 2.6內核)

    目錄 一、設備類型 二、模塊 構建模塊 安裝模塊 載入模塊 在 Linux 以及 Unix 系統(tǒng)中,設備被分為以下三種類型: 塊設備(blkdev) :以塊為尋址單位,塊的大小隨設備的不同而變化;塊設備通常支持重定位(seeking)操作,也就是對數據的隨機訪問。如硬盤、藍光光碟和 Flas

    2024年02月11日
    瀏覽(87)
  • Linux驅動開發(fā)——內核模塊

    Linux驅動開發(fā)——內核模塊

    目錄 內核模塊的由來 第一個內核模塊程序? 內核模塊工具? 將多個源文件編譯生成一個內核模塊? 內核模塊參數 內核模塊依賴 關于內核模塊的進一步討論? 習題 最近一直在玩那些其它的技術,眼看快暑假了,我決定夯實一下我的驅動方面的技能,迎接我的實習,找了一本

    2024年02月04日
    瀏覽(97)
  • Linux內核顯示、加載、卸載等超實用命令

    內核模塊是 Linux 系統(tǒng)中一種特殊的可執(zhí)行文件,它可以在運行時動態(tài)地加載到內核中或卸載出內核,從而實現內核的擴展和優(yōu)化。內核模塊操作相關的命令主要有以下幾種: 列出當前已加載的內核模塊及其依賴關系和使用情況。 將指定的內核模塊加載到內核中,需要提供完

    2024年02月08日
    瀏覽(93)
  • linux驅動開發(fā)--day1(驅動、內核模塊及相關命令、內核模塊傳參)
  • Linux內核移植:內核的啟動過程分析、啟動配置與rootfs必要文件

    ?內核啟動通常包括4個階段: iROM代碼啟動(BIOS啟動)。開發(fā)板上電后,先執(zhí)行內部iROM中的固化代碼,類似于BIOS,執(zhí)行通電自檢和初始化過程,包括初始化CPU、存儲器、時鐘、總線等一些必要的硬件資源。 啟動引導加載程序BootLoader。根據啟動引腳的電平,讀取相應的存儲

    2024年02月13日
    瀏覽(767)
  • 【嵌入式Linux內核驅動】內核模塊三要素與驗證測試

    內核模塊 Linux內核模塊是一種可以動態(tài)加載和卸載的軟件組件,用于擴展Linux操作系統(tǒng)的功能。Linux內核本身只包含了必要的核心功能,而內核模塊則允許開發(fā)者在運行時向內核添加新的功能、驅動程序或文件系統(tǒng)支持,而無需重新編譯整個內核或重新啟動系統(tǒng)。 內核模塊是

    2024年02月06日
    瀏覽(130)
  • 修改linux的/sys目錄下內核參數、模塊...

    ① /sys/devices 該目錄下是全局設備結構體系,包含所有被發(fā)現的注冊在各種總線上的各種物理設備。一般來說,所有的物理設備都按其在總線上的拓撲結構來顯示,但有兩個例外,即platform devices和system devices。platform devices一般是掛在芯片內部的高速或者低速總線上的各種控制

    2024年02月05日
    瀏覽(89)
  • 【Linux驅動】內核模塊編譯 —— make modules 的使用(單模塊編譯、多模塊編譯)

    編譯驅動一般采用的是將驅動編譯成模塊(.ko 文件),然后加載到內核,這其中就用到了 make modules 命令。 目錄 一、單模塊編譯 1、一個 c 文件編譯成一個 ko 文件 2、多個文件編譯成一個 ko 文件 二、多模塊編譯(多文件多模塊) 下面是最簡易的單文件單模塊編譯,假設我們

    2024年02月10日
    瀏覽(88)
  • Linux學習之Ubuntu 20.04安裝內核模塊

    Linux學習之Ubuntu 20.04安裝內核模塊

    參考博客:Ubuntu20.04編譯內核教程 sudo lsb_release -a 可以看到我當前的系統(tǒng)是 Ubuntu 20.04.4 , sudo uname -r 可以看到我的系統(tǒng)內核版本是 5.4.0-100-generic 。 sudo apt-get install -y libncurses5-dev flex bison libssl-dev 安裝所需要的依賴。 sudo apt-get install linux-source 按兩下 Tab ,看一下可以下載的源

    2024年02月15日
    瀏覽(124)
  • Linux 編譯內核模塊出現--Unknown symbol mcount

    Linux suse: 在編譯SUSE Linux Enterprise Server 12 SP時,使用低版本的docker鏡像編譯內核模塊時,加載內核模塊時出現: 加載內核模塊時: (1) 指示系統(tǒng)可能受到 Spectre V2 漏洞的影響,并且正在加載的模塊沒有使用 retpoline 編譯器進行編譯。 Spectre V2(CVE-2017-5715)是 Spectre 漏洞家族

    2024年02月11日
    瀏覽(85)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包