其他
自定义Linker实现分析之路
前言
bionic/linker
目录下,通过对Linker源码的学习、分析,我们也可以实现一个自定义的Linker加载器。Linker的加载过程
1.读取程序头表,获取动态段信息
2.加载动态段中的so文件
3.进行重定位操作
4.初始化so文件中的全局变量
5.调用so文件中的初始化函数
6.结束
Linker源码核心函数详解
int flags,
const android_dlextinfo* extinfo,
const void* caller_addr) {
void* result = do_dlopen(filename, flags, extinfo, caller_addr);
return result;
}
bionic/linker/linker.cpp
文件中。const android_dlextinfo* extinfo,
const void* caller_addr) {
...
soinfo* const caller = find_containing_library(caller_addr);
android_namespace_t* ns = get_caller_namespace(caller);
...
...
soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
loading_trace.End();
if (si != nullptr) {
void* handle = si->to_handle();
LD_LOG(kLogDlopen,
"... dlopen calling constructors: realpath=\"%s\", soname=\"%s\", handle=%p",
si->get_realpath(), si->get_soname(), handle);
si->call_constructors();
failure_guard.Disable();
LD_LOG(kLogDlopen,
"... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p",
si->get_realpath(), si->get_soname(), handle);
return handle;
}
return nullptr;
}
这里有个 android_namespace_t* ns = get_caller_namespace(caller);
这个函数是获取调用者的namespace,namespace是Android 7.0引入的概念,用于解决so文件的命名冲突问题,这里不做详细讨论。
const char* name, int rtld_flags,
const android_dlextinfo* extinfo,
soinfo* needed_by) {
soinfo* si = nullptr;
if (name == nullptr) {
si = solist_get_somain();
} else if (!find_libraries(ns,
needed_by,
&name,
1,
&si,
nullptr,
0,
rtld_flags,
extinfo,
false /* add_as_children */)) {
if (si != nullptr) {
soinfo_unload(si);
}
return nullptr;
}
si->increment_ref_count();
return si;
}
}
soinfo *start_with,
const char *const library_names[],
size_t library_names_count,
soinfo *soinfos[],
std::vector<soinfo *> *ld_preloads,
size_t ld_preloads_count,
int rtld_flags,
const android_dlextinfo *extinfo,
bool add_as_children,
std::vector<android_namespace_t *> *namespaces = nullptr) {
...
// Step 0: prepare.
// 第一步开始准备 加入一个tasks list里
std::unordered_map<const soinfo *, ElfReader> readers_map;
LoadTaskList load_tasks;
for (size_t i = 0; i < library_names_count; ++i) {
const char *name = library_names[i];
LOGI("load task create %s ", name);
load_tasks.push_back(LoadTask::create(name, start_with, ns, &readers_map));
}
}
...
//这一步是动态链接过程中的一个重要环节,其目的是扩展要加载的库(load_tasks)列表,
// 以包括所有通过DT_NEEDED条目指定的依赖库。DT_NEEDED条目是在ELF(Executable and Linkable Format)文件的动态段中指定的,
// 表示当前库需要加载的其他库。这一步骤并不立即加载这些依赖库,而是准备加载任务
for (size_t i = 0; i < load_tasks.size(); ++i) {
LoadTask *task = load_tasks[i];
soinfo *needed_by = task->get_needed_by();
bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
task->set_extinfo(is_dt_needed ? nullptr : extinfo);
task->set_dt_needed(is_dt_needed);
LOGI("find_libraries(ns=%s): task=%s, is_dt_needed=%d", "null",
task->get_name(), is_dt_needed);
//
// Note: start from the namespace that is stored in the LoadTask. This namespace
// is different from the current namespace when the LoadTask is for a transitive
// dependency and the lib that created the LoadTask is not found in the
// current namespace but in one of the linked namespace.
if (!find_library_internal(const_cast<android_namespace_t *>(task->get_start_from()),
task,
&zip_archive_cache,
&load_tasks,
rtld_flags)) {
return false;
}
}
LoadTask *task,
ZipArchiveCache *zip_archive_cache,
LoadTaskList *load_tasks,
int rtld_flags) {
soinfo *candidate;
if (find_loaded_library_by_soname(ns, task->get_name(), true /* search_linked_namespaces */,
&candidate)) {
LOGI(
"find_library_internal(ns=%s, task=%s): Already loaded (by soname): %s",
ns->get_name(), task->get_name(), candidate->get_realpath());
task->set_soinfo(candidate);
return true;
}
//start load_library
if (load_library(ns, task, zip_archive_cache, load_tasks, rtld_flags,
true /* search_linked_namespaces */)) {
return true;
}
return false;
}
LoadTask *task,
ZipArchiveCache *zip_archive_cache,
LoadTaskList *load_tasks,
int rtld_flags,
bool search_linked_namespaces) {
const char *name = task->get_name();
soinfo *needed_by = task->get_needed_by();
//use dlopen extinfo is null; so we dont need extinfo code
LOGI(
"load_library(ns=%s, task=%s, flags=0x%x, search_linked_namespaces=%d): calling "
"open_library",
ns->get_name(), name, rtld_flags, search_linked_namespaces);
// Open the file.
off64_t file_offset;
std::string realpath;
int fd = open_library(ns, zip_archive_cache, name, needed_by, &file_offset, &realpath);
if (fd == -1) {
LOGE("library \"%s\" not found", name);
return false;
}
task->set_fd(fd, true);
task->set_file_offset(file_offset);
return load_library(ns, task, load_tasks, rtld_flags, realpath, search_linked_namespaces);
}
LoadTask *task,
LoadTaskList *load_tasks,
int rtld_flags,
const std::string &realpath,
bool search_linked_namespaces) {
...
// we dont need accessible file so ignore this code
// if ((fs_stat.f_type != TMPFS_MAGIC) && (!ns->is_accessible(realpath))) {
soinfo *si = soinfo_alloc(ns, realpath.c_str(), &file_stat, file_offset, rtld_flags);
task->set_soinfo(si);
// Read the ELF header and some of the segments.
if (!task->read(realpath.c_str(), file_stat.st_size)) {
task->remove_cached_elf_reader();
task->set_soinfo(nullptr);
soinfo_free(si);
return false;
}
...
for_each_dt_needed(task->get_elf_reader(), [&](const char* name) {
LD_LOG(kLogDlopen, "load_library(ns=%s, task=%s): Adding DT_NEEDED task: %s",
ns->get_name(), task->get_name(), name);
load_tasks->push_back(LoadTask::create(name, si, ns, task->get_readers_map()));
});
}
task->read(realpath.c_str(), file_stat.st_size)
bionic/linker/linker_phdr.cpp
文件中。if (did_read_) {
return true;
}
name_ = name;
fd_ = fd;
file_offset_ = file_offset;
file_size_ = file_size;
if (ReadElfHeader() &&
VerifyElfHeader() &&
ReadProgramHeaders() &&
ReadSectionHeaders() &&
ReadDynamicSection()) {
did_read_ = true;
}
return did_read_;
}
我们需要看一下另外一个实现for_each_dt_needed(task->get_elf_reader(), [&](const char* name) 这个对我们自定义linker来说实现上是有区别的,
在linker源码中,这个函数的作用是获取so文件中的DT_NEEDED字段,然后将DT_NEEDED字段中的so文件加入到加载列表中,由于简化流程考虑,这里我的实现直接用dlopen打开系统库。(不包含链接第三方库so的情况)
// our so need load so
//we use dlopen
void *pVoid = dlopen(name, RTLD_NOW);
});
LOGI(" after find_library_internal call is_dt_needed %s %i", task->get_name(),
is_dt_needed);
if (is_dt_needed) {
needed_by->add_child(si);
}
...
address_space_params default_params;
size_t relro_fd_offset = 0;
for (auto &&task: load_list) {
address_space_params *address_space = &default_params;
if (!task->load(address_space)) {
return false;
}
}
ElfReader &elf_reader = get_elf_reader();
if (!elf_reader.Load(address_space)) {
return false;
}
si_->base = elf_reader.load_start();
si_->size = elf_reader.load_size();
si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller());
si_->load_bias = elf_reader.load_bias();
si_->phnum = elf_reader.phdr_count();
si_->phdr = elf_reader.loaded_phdr();
si_->set_gap_start(elf_reader.gap_start());
si_->set_gap_size(elf_reader.gap_size());
return true;
}
if (did_load_) {
return true;
}
if (ReserveAddressSpace(address_space) && LoadSegments()
&& FindPhdr() && FindGnuPropertySection()) {
did_load_ = true;
#if defined(__aarch64__)
// For Armv8.5-A loaded executable segments may require PROT_BTI.
LOGI("isBTICompatible = %d", note_gnu_property_.IsBTICompatible());
if (note_gnu_property_.IsBTICompatible()) {
did_load_ = (phdr_table_protect_segments(phdr_table_, phdr_num_, load_bias_,
¬e_gnu_property_) == 0);
}
#endif
}
return did_load_;
}
并尝试在进程的地址空间中预留这块区域。这一步是将ELF文件从磁盘映射到内存中的前置工作。
接下来的是LoadSegments()。
void* seg_addr = mmap64(reinterpret_cast<void*>(seg_page_start),
file_length,
prot,
MAP_FIXED|MAP_PRIVATE,
fd_,
file_offset_ + file_page_start);
if (seg_addr == MAP_FAILED) {
DL_ERR("couldn't map \"%s\" segment %zd: %s", name_.c_str(), i, strerror(errno));
return false;
}
这里的实现可以拓展下,我们可知mmap如果传入文件描述符,那么最终文件的路径会在/proc/self/maps中显示,这里其实可以通过另外的方式实现,从而完成so文件的隐藏。
// If there is a PT_PHDR, use it directly.
// If there is a PT_PHDR, use it directly.
for (const ElfW(Phdr) *phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
if (phdr->p_type == PT_PHDR) {
return CheckPhdr(load_bias_ + phdr->p_vaddr);
}
}
// Otherwise, check the first loadable segment. If its file offset
// is 0, it starts with the ELF header, and we can trivially find the
// loaded program header from it.
for (const ElfW(Phdr) *phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
if (phdr->p_type == PT_LOAD) {
if (phdr->p_offset == 0) {
ElfW(Addr) elf_addr = load_bias_ + phdr->p_vaddr;
const ElfW(Ehdr) *ehdr = reinterpret_cast<const ElfW(Ehdr) *>(elf_addr);
ElfW(Addr) offset = ehdr->e_phoff;
return CheckPhdr(reinterpret_cast<ElfW(Addr)>(ehdr) + offset);
}
break;
}
}
的内存地址。这一步骤对于后续的库重定位和初始化至关重要。
在ELF文件格式中,程序头表描述了文件的段(比如代码段、数据段等)如何映射到进程的虚拟地址空间中。
不同于phdr_table_,这是一个临时的、在链接器内部使用的拷贝,loaded_phdr_指向的是映射到内存中、即将被实际使用的程序头表的地址。
ImyLinker(https://github.com/IIIImmmyyy/ImyLinker)
看雪ID:IIImmmyyy
https://bbs.kanxue.com/user-home-810816.htm
# 往期推荐
2、阿里云CTF2024-暴力ENOTYOURWORLD题解
3、Hypervisor From Scratch - 基本概念和配置测试环境、进入 VMX 操作
5、套娃之arm架构下的MacBook通过parallels+rosetta安装Linux amd64版本的IDA Pro
球分享
球点赞
球在看
点击阅读原文查看更多