我们跟踪创建Loadtask过程。
该函数调用LoadTask的重载函数。
该函数完成各个成员的赋值,简单看下哪些成员被赋值,name,needed_by ,elf_readermap , start_from.
start_with
追溯needed_by 来历, needed_by传入时实参为start_with , 继续往上跟,发现caller_addr 通过__builtin_return_address来赋值。
查看__builtin_return_address() 的说明 , 返回调用函数的地址,g++的内建函数 ,可以获取到调用函数时的ret地址。就可以获取到调用android_dlopen_exit的函数地址。
在该函数将其转换为soinfo 。
find_containing_library 根据地址的偏移计算来返回调用函数所在模块的soinfo 。
我们跟踪创建Loadtask过程。
该函数调用LoadTask的重载函数 , 同时分配空间。
该函数完成各个成员的赋值,简单看下哪些成员被赋值,名称,needed_by ,elf_readermap , start_from 。
readers_map 就有意思了 , 在调用load过程中可以用来解析elf文件。
该过程在find_library中被生成 。
该步骤给列表中增加so。
筛选要载入的load_list 并解析elf文件的so
这个elf_reader 就是对应so用来解析elf文件的。
Dynamic entry
预连接所有DT_NEEDED 库, 逻辑简单,没有被连接过,则调用prelink_image()。
[x] prelink_image。 依次看全太肝了 , 后期在这里查字段。
略
涉及到重定位的镜像连接。
在Step3中找这rel_ 和pltrel 的赋值,分别为DT_JMPREL 和 DT_REL 对应.rel.plt 和.rel.dyn段 。
参数中有 plain_reloc_iterator ,传入rel的迭代器 。
Elf32_Rel的结构体。
对于DT_REL,该函数获取到rel.dyn 段, 到这里重定位的内容就很简单了。
start: =
addr : = 0
重定位的类型:
重定位运算方法:
验证重定位过程。
以类型为R_ARM_RELATIVE 为例。
偏移为1a610 类型为R_ARM_RELATIVE ,该偏移的值为19758 , 则修正 base+1a610 的地址的值为 19758 + base。
重定位结果。
但是重定位算法解决了,但是对于不同的类型含义是什么?? 可以确定R_ARM_RELATIVE 对应内部符号重定位 , R_ARM_GLOB_DAT,R_ARM_JMP_SLOT对应外部符号 ,其中R_ARM_GLOB_DAT和R_ARM_JMP_SLOT的差别是??
https://bbs.pediy.com/upload/attach/202012/790193_GT99SKKNDDFX66S.png 连接过程结束后,在依次返回到上层函数,就可以明白一个so的加载过程为 分配空间-> 解析elf -> 重定位空间 -> 调用init -> 存在则调用Jni_Onload .
https://docs.oracle.com/cd/E19120-01/open.solaris/819-0690/chapter6-26/index.html 重定位类型
http://nicephil.blinkenshell.org/my_book/ch07s04.html ELF 相关知识
/
/
Step
0
: prepare.
LoadTaskList load_tasks;
for
(size_t i
=
0
; i < library_names_count;
+
+
i) {
const char
*
name
=
library_names[i];
LD_LOG(kLogDlopen,
"[linker.cpp] step 1 ,so_name"
,name);
load_tasks.push_back(LoadTask::create(name, start_with, ns, &readers_map));
}
/
/
If soinfos array
is
null allocate one on stack.
/
/
The array
is
needed
in
case of failure;
for
example
/
/
when library_names[]
=
{libone.so, libtwo.so}
and
libone.so
/
/
is
loaded correctly but libtwo.so failed
for
some reason.
/
/
In this case libone.so should be unloaded on
return
.
/
/
See also implementation of failure_guard below.
if
(soinfos
=
=
nullptr) {
size_t soinfos_size
=
sizeof(soinfo
*
)
*
library_names_count;
soinfos
=
reinterpret_cast<soinfo
*
*
>(alloca(soinfos_size));
memset(soinfos,
0
, soinfos_size);
}
/
/
list
of libraries to link
-
see step
2.
size_t soinfos_count
=
0
;
auto scope_guard
=
android::base::make_scope_guard([&]() {
for
(LoadTask
*
t : load_tasks) {
LD_LOG(kLogDlopen,
"[linker.cpp] before call deleter %s"
,t
-
>get_name());
LoadTask::deleter(t);
}
});
auto failure_guard
=
android::base::make_scope_guard([&]() {
/
/
Housekeeping
soinfo_unload(soinfos, soinfos_count);
});
ZipArchiveCache zip_archive_cache;
/
/
Step
0
: prepare.
LoadTaskList load_tasks;
for
(size_t i
=
0
; i < library_names_count;
+
+
i) {
const char
*
name
=
library_names[i];
LD_LOG(kLogDlopen,
"[linker.cpp] step 1 ,so_name"
,name);
load_tasks.push_back(LoadTask::create(name, start_with, ns, &readers_map));
}
/
/
If soinfos array
is
null allocate one on stack.
/
/
The array
is
needed
in
case of failure;
for
example
/
/
when library_names[]
=
{libone.so, libtwo.so}
and
libone.so
/
/
is
loaded correctly but libtwo.so failed
for
some reason.
/
/
In this case libone.so should be unloaded on
return
.
/
/
See also implementation of failure_guard below.
if
(soinfos
=
=
nullptr) {
size_t soinfos_size
=
sizeof(soinfo
*
)
*
library_names_count;
soinfos
=
reinterpret_cast<soinfo
*
*
>(alloca(soinfos_size));
memset(soinfos,
0
, soinfos_size);
}
/
/
list
of libraries to link
-
see step
2.
size_t soinfos_count
=
0
;
auto scope_guard
=
android::base::make_scope_guard([&]() {
for
(LoadTask
*
t : load_tasks) {
LD_LOG(kLogDlopen,
"[linker.cpp] before call deleter %s"
,t
-
>get_name());
LoadTask::deleter(t);
}
});
auto failure_guard
=
android::base::make_scope_guard([&]() {
/
/
Housekeeping
soinfo_unload(soinfos, soinfos_count);
});
ZipArchiveCache zip_archive_cache;
static LoadTask
*
create(const char
*
name,
soinfo
*
needed_by,
android_namespace_t
*
start_from,
std::unordered_map<const soinfo
*
, ElfReader>
*
readers_map) {
LoadTask
*
ptr
=
TypeBasedAllocator<LoadTask>::alloc();
return
new (ptr) LoadTask(name, needed_by, start_from, readers_map);
}
static LoadTask
*
create(const char
*
name,
soinfo
*
needed_by,
android_namespace_t
*
start_from,
std::unordered_map<const soinfo
*
, ElfReader>
*
readers_map) {
LoadTask
*
ptr
=
TypeBasedAllocator<LoadTask>::alloc();
return
new (ptr) LoadTask(name, needed_by, start_from, readers_map);
}
LoadTask(const char
*
name,
soinfo
*
needed_by,
android_namespace_t
*
start_from,
std::unordered_map<const soinfo
*
, ElfReader>
*
readers_map)
: name_(name), needed_by_(needed_by), si_(nullptr),
fd_(
-
1
), close_fd_(false), file_offset_(
0
), elf_readers_map_(readers_map),
is_dt_needed_(false), start_from_(start_from) {}
LoadTask(const char
*
name,
soinfo
*
needed_by,
android_namespace_t
*
start_from,
std::unordered_map<const soinfo
*
, ElfReader>
*
readers_map)
: name_(name), needed_by_(needed_by), si_(nullptr),
fd_(
-
1
), close_fd_(false), file_offset_(
0
), elf_readers_map_(readers_map),
is_dt_needed_(false), start_from_(start_from) {}
void
*
android_dlopen_ext(const char
*
filename,
int
flag, const android_dlextinfo
*
extinfo) {
const void
*
caller_addr
=
__builtin_return_address(
0
);
return
__loader_android_dlopen_ext(filename, flag, extinfo, caller_addr);
}
void
*
android_dlopen_ext(const char
*
filename,
int
flag, const android_dlextinfo
*
extinfo) {
const void
*
caller_addr
=
__builtin_return_address(
0
);
return
__loader_android_dlopen_ext(filename, flag, extinfo, caller_addr);
}
soinfo
*
find_containing_library(const void
*
p) {
ElfW(Addr) address
=
reinterpret_cast<ElfW(Addr)>(p);
for
(soinfo
*
si
=
solist_get_head(); si !
=
nullptr; si
=
si
-
>
next
) {
if
(address >
=
si
-
>base && address
-
si
-
>base < si
-
>size) {
return
si;
}
}
return
nullptr;
}
soinfo
*
find_containing_library(const void
*
p) {
ElfW(Addr) address
=
reinterpret_cast<ElfW(Addr)>(p);
for
(soinfo
*
si
=
solist_get_head(); si !
=
nullptr; si
=
si
-
>
next
) {
if
(address >
=
si
-
>base && address
-
si
-
>base < si
-
>size) {
return
si;
}
}
return
nullptr;
}
static LoadTask
*
create(const char
*
name,
soinfo
*
needed_by,
android_namespace_t
*
start_from,
std::unordered_map<const soinfo
*
, ElfReader>
*
readers_map) {
LoadTask
*
ptr
=
TypeBasedAllocator<LoadTask>::alloc();
return
new (ptr) LoadTask(name, needed_by, start_from, readers_map);
}
static LoadTask
*
create(const char
*
name,
soinfo
*
needed_by,
android_namespace_t
*
start_from,
std::unordered_map<const soinfo
*
, ElfReader>
*
readers_map) {
LoadTask
*
ptr
=
TypeBasedAllocator<LoadTask>::alloc();
return
new (ptr) LoadTask(name, needed_by, start_from, readers_map);
}
LoadTask(const char
*
name,
soinfo
*
needed_by,
android_namespace_t
*
start_from,
std::unordered_map<const soinfo
*
, ElfReader>
*
readers_map)
: name_(name), needed_by_(needed_by), si_(nullptr),
fd_(
-
1
), close_fd_(false), file_offset_(
0
), elf_readers_map_(readers_map),
is_dt_needed_(false), start_from_(start_from) {}
LoadTask(const char
*
name,
soinfo
*
needed_by,
android_namespace_t
*
start_from,
std::unordered_map<const soinfo
*
, ElfReader>
*
readers_map)
: name_(name), needed_by_(needed_by), si_(nullptr),
fd_(
-
1
), close_fd_(false), file_offset_(
0
), elf_readers_map_(readers_map),
is_dt_needed_(false), start_from_(start_from) {}
/
/
readers_map
is
shared across recursive calls to find_libraries.
std::unordered_map<const soinfo
*
, ElfReader> readers_map;
/
/
readers_map
is
shared across recursive calls to find_libraries.
std::unordered_map<const soinfo
*
, ElfReader> readers_map;
/
/
Step
1
: expand the
list
of load_tasks to include
/
/
all
DT_NEEDED libraries (do
not
load them just yet)
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);
/
*
try
to find the load.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,
search_linked_namespaces || is_dt_needed)) {
return
false;
}
soinfo
*
si
=
task
-
>get_soinfo();
if
(is_dt_needed) {
needed_by
-
>add_child(si);
if
(si
-
>is_linked()) {
si
-
>increment_ref_count();
}
}
/
/
When ld_preloads
is
not
null, the first
/
/
ld_preloads_count libs are
in
fact ld_preloads.
if
(ld_preloads !
=
nullptr && soinfos_count < ld_preloads_count) {
ld_preloads
-
>push_back(si);
}
if
(soinfos_count < library_names_count) {
soinfos[soinfos_count
+
+
]
=
si;
}
}
/
/
Step
1
: expand the
list
of load_tasks to include
/
/
all
DT_NEEDED libraries (do
not
load them just yet)
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);
/
*
try
to find the load.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,
search_linked_namespaces || is_dt_needed)) {
return
false;
}
soinfo
*
si
=
task
-
>get_soinfo();
if
(is_dt_needed) {
needed_by
-
>add_child(si);
if
(si
-
>is_linked()) {
si
-
>increment_ref_count();
}
}
/
/
When ld_preloads
is
not
null, the first
/
/
ld_preloads_count libs are
in
fact ld_preloads.
if
(ld_preloads !
=
nullptr && soinfos_count < ld_preloads_count) {
ld_preloads
-
>push_back(si);
}
if
(soinfos_count < library_names_count) {
soinfos[soinfos_count
+
+
]
=
si;
}
}
/
/
Step
2
: Load libraries
in
random order (see b
/
24047022
)
LoadTaskList load_list;
for
(auto&& task : load_tasks) {
soinfo
*
si
=
task
-
>get_soinfo();
auto pred
=
[&](const LoadTask
*
t) {
return
t
-
>get_soinfo()
=
=
si;
};
if
(!si
-
>is_linked() &&
std::find_if(load_list.begin(), load_list.end(), pred)
=
=
load_list.end() ) {
load_list.push_back(task);
}
}
shuffle(&load_list);
for
(auto&& task : load_list) {
/
/
*
*
*
看看这个loda函数
if
(!task
-
>load()) {
return
false;
}
}
/
/
Step
2
: Load libraries
in
random order (see b
/
24047022
)
LoadTaskList load_list;
for
(auto&& task : load_tasks) {
soinfo
*
si
=
task
-
>get_soinfo();
auto pred
=
[&](const LoadTask
*
t) {
return
t
-
>get_soinfo()
=
=
si;
};
if
(!si
-
>is_linked() &&
std::find_if(load_list.begin(), load_list.end(), pred)
=
=
load_list.end() ) {
load_list.push_back(task);
}
}
shuffle(&load_list);
for
(auto&& task : load_list) {
/
/
*
*
*
看看这个loda函数
if
(!task
-
>load()) {
return
false;
}
}
bool
load() {
ElfReader& elf_reader
=
get_elf_reader();
/
/
在elf_read.Load 下完成elf文件空间的分配和解析 ,还不能算载入了so。
if
(!elf_reader.Load(extinfo_)) {
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();
LD_LOG(kLogDlopen,
"[linker.cpp] Step2 load_bias %p "
,si_
-
>load_bias);
si_
-
>phnum
=
elf_reader.phdr_count();
si_
-
>phdr
=
elf_reader.loaded_phdr();
return
true;
}
bool
load() {
ElfReader& elf_reader
=
get_elf_reader();
/
/
在elf_read.Load 下完成elf文件空间的分配和解析 ,还不能算载入了so。
if
(!elf_reader.Load(extinfo_)) {
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();
LD_LOG(kLogDlopen,
"[linker.cpp] Step2 load_bias %p "
,si_
-
>load_bias);
si_
-
>phnum
=
elf_reader.phdr_count();
si_
-
>phdr
=
elf_reader.loaded_phdr();
return
true;
}
typedef struct
{
Elf64_Sxword d_tag;
/
*
Dynamic entry
type
*
/
union
{
Elf64_Xword d_val;
/
*
Integer value
*
/
Elf64_Addr d_ptr;
/
*
Address value
*
/
} d_un;
} Elf64_Dyn;
typedef struct
{
Elf64_Sxword d_tag;
/
*
Dynamic entry
type
*
/
union
{
Elf64_Xword d_val;
/
*
Integer value
*
/
Elf64_Addr d_ptr;
/
*
Address value
*
/
} d_un;
} Elf64_Dyn;
/
/
Step
3
: pre
-
link
all
DT_NEEDED libraries
in
breadth first order.
for
(auto&& task : load_tasks) {
soinfo
*
si
=
task
-
>get_soinfo();
if
(!si
-
>is_linked() && !si
-
>prelink_image()) {
return
false;
}
}
/
/
Step
3
: pre
-
link
all
DT_NEEDED libraries
in
breadth first order.
for
(auto&& task : load_tasks) {
soinfo
*
si
=
task
-
>get_soinfo();
if
(!si
-
>is_linked() && !si
-
>prelink_image()) {
return
false;
}
}
/
/
Step
5
: link libraries that are
not
destined to this namespace.
/
/
Do this by recursively calling find_libraries on the namespace where the lib
/
/
was found during Step
1.
for
(auto&& task : load_tasks) {
soinfo
*
si
=
task
-
>get_soinfo();
if
(si
-
>get_primary_namespace() !
=
ns) {
const char
*
name
=
task
-
>get_name();
if
(find_libraries(si
-
>get_primary_namespace(), task
-
>get_needed_by(), &name,
1
,
nullptr
/
*
soinfos
*
/
, nullptr
/
*
ld_preloads
*
/
,
0
/
*
ld_preload_count
*
/
,
rtld_flags, nullptr
/
*
extinfo
*
/
, false
/
*
add_as_children
*
/
,
false
/
*
search_linked_namespaces
*
/
, readers_map, namespaces)) {
/
/
If this lib
is
directly needed by one of the libs
in
this namespace,
/
/
then increment the count
soinfo
*
needed_by
=
task
-
>get_needed_by();
if
(needed_by !
=
nullptr && needed_by
-
>get_primary_namespace()
=
=
ns && si
-
>is_linked()) {
si
-
>increment_ref_count();
}
}
else
{
return
false;
}
}
}
/
/
连接这个命名空间的库文件
/
/
Step
6
: link libraries
in
this namespace
soinfo_list_t local_group;
walk_dependencies_tree(
(start_with !
=
nullptr && add_as_children) ? &start_with : soinfos,
(start_with !
=
nullptr && add_as_children) ?
1
: soinfos_count,
[&] (soinfo
*
si) {
if
(ns
-
>is_accessible(si)) {
local_group.push_back(si);
return
kWalkContinue;
}
else
{
return
kWalkSkip;
}
});
soinfo_list_t global_group
=
ns
-
>get_global_group();
bool
linked
=
local_group.visit([&](soinfo
*
si) {
if
(!si
-
>is_linked()) {
LD_LOG(kLogDlopen,
"so %s is not linked , now try to link "
,si
-
>get_soname());
if
(!si
-
>link_image(global_group, local_group, extinfo) ||
!get_cfi_shadow()
-
>AfterLoad(si, solist_get_head())) {
return
false;
}
}
return
true;
});
if
(linked) {
local_group.for_each([](soinfo
*
si) {
LD_LOG(kLogDlopen,
"travser local_group list %s"
, si
-
>get_soname());
if
(!si
-
>is_linked()) {
si
-
>set_linked();
}
});
failure_guard.Disable();
}
return
linked;
/
/
Step
5
: link libraries that are
not
destined to this namespace.
/
/
Do this by recursively calling find_libraries on the namespace where the lib
/
/
was found during Step
1.
for
(auto&& task : load_tasks) {
soinfo
*
si
=
task
-
>get_soinfo();
if
(si
-
>get_primary_namespace() !
=
ns) {
const char
*
name
=
task
-
>get_name();
if
(find_libraries(si
-
>get_primary_namespace(), task
-
>get_needed_by(), &name,
1
,
nullptr
/
*
soinfos
*
/
, nullptr
/
*
ld_preloads
*
/
,
0
/
*
ld_preload_count
*
/
,
rtld_flags, nullptr
/
*
extinfo
*
/
, false
/
*
add_as_children
*
/
,
false
/
*
search_linked_namespaces
*
/
, readers_map, namespaces)) {
/
/
If this lib
is
directly needed by one of the libs
in
this namespace,
/
/
then increment the count
soinfo
*
needed_by
=
task
-
>get_needed_by();
if
(needed_by !
=
nullptr && needed_by
-
>get_primary_namespace()
=
=
ns && si
-
>is_linked()) {
si
-
>increment_ref_count();
}
}
else
{
return
false;
}
}
}
/
/
连接这个命名空间的库文件
/
/
Step
6
: link libraries
in
this namespace
soinfo_list_t local_group;
walk_dependencies_tree(
(start_with !
=
nullptr && add_as_children) ? &start_with : soinfos,
(start_with !
=
nullptr && add_as_children) ?
1
: soinfos_count,
[&] (soinfo
*
si) {
if
(ns
-
>is_accessible(si)) {
local_group.push_back(si);
return
kWalkContinue;
}
else
{
return
kWalkSkip;
}
});
soinfo_list_t global_group
=
ns
-
>get_global_group();
bool
linked
=
local_group.visit([&](soinfo
*
si) {
if
(!si
-
>is_linked()) {
LD_LOG(kLogDlopen,
"so %s is not linked , now try to link "
,si
-
>get_soname());
if
(!si
-
>link_image(global_group, local_group, extinfo) ||
!get_cfi_shadow()
-
>AfterLoad(si, solist_get_head())) {
return
false;
}
}
return
true;
});
if
(linked) {
local_group.for_each([](soinfo
*
si) {
LD_LOG(kLogDlopen,
"travser local_group list %s"
, si
-
>get_soname());
if
(!si
-
>is_linked()) {
si
-
>set_linked();
}
});
failure_guard.Disable();
}
return
linked;
bool
soinfo::link_image(const soinfo_list_t& global_group, const soinfo_list_t& local_group,
const android_dlextinfo
*
extinfo) {
/
/
这里仅保留了大家比较熟悉的类型。常规都是rel。
if
(rela_ !
=
nullptr) {
DEBUG(
"[ relocating %s ]"
, get_realpath());
if
(!relocate(version_tracker,
plain_reloc_iterator(rela_, rela_count_), global_group, local_group)) {
return
false;
}
}
if
(plt_rela_ !
=
nullptr) {
DEBUG(
"[ relocating %s plt ]"
, get_realpath());
if
(!relocate(version_tracker,
plain_reloc_iterator(plt_rela_, plt_rela_count_), global_group, local_group)) {
return
false;
}
}
/
/
把重心放在这里
if
( !
=
nullptr) {
DEBUG(
"[ relocating %s ]"
, get_realpath());
if
(!relocate(version_tracker,
plain_reloc_iterator(rel_, rel_count_), global_group, local_group)) {
return
false;
}
}
if
(plt_rel_ !
=
nullptr) {
LD_LOG(kLogDlopen,
"[ relocating %s plt ]"
, get_realpath());
if
(!relocate(version_tracker,
plain_reloc_iterator(plt_rel_, plt_rel_count_), global_group, local_group)) {
return
false;
}
}
bool
soinfo::link_image(const soinfo_list_t& global_group, const soinfo_list_t& local_group,
const android_dlextinfo
*
extinfo) {
/
/
这里仅保留了大家比较熟悉的类型。常规都是rel。
if
(rela_ !
=
nullptr) {
DEBUG(
"[ relocating %s ]"
, get_realpath());
if
(!relocate(version_tracker,
plain_reloc_iterator(rela_, rela_count_), global_group, local_group)) {
return
false;
}
}
if
(plt_rela_ !
=
nullptr) {
DEBUG(
"[ relocating %s plt ]"
, get_realpath());
if
(!relocate(version_tracker,
plain_reloc_iterator(plt_rela_, plt_rela_count_), global_group, local_group)) {
return
false;
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2020-12-31 16:51
被pareto编辑
,原因:
上传的附件: