系列文章目录
文章目录
前言
一、
dlopen, dlerror, dlclose
#include <dlfcn.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
void* handle;
using fty = double(*)(double);
fty func;
char* error;
handle = dlopen(argv[1], RTLD_NOW);
if (handle == nullptr) {
printf("%s\n", dlerror());
return -1;
}
func = (fty) dlsym(handle, "sin");
if ( (error = dlerror()) != nullptr ) {
printf("Symbol sin not found: %s\n", error);
goto exit_runso;
}
printf("%f\n", func(3.1415926 / 2));
exit_runso:
dlclose(handle);
}
直接执行动态库中的某个函数/某段代码
./a.out /libxxx.so funname arg1 arg2 ... return_type
编译不通过且看不懂
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#define SETUP_STACK \
i = 2; \
while (++i < argc - 1) \
{ \
switch (argv[i][0]) \
{ \
case 'i':\
asm volatile("push %0" ::\
"r"(atoi(&argv[i][1])) ); \
esp += 4; \
break; \
case 'd': \
atof(&argv[i][1]); \
asm volatile("subl $8, %esp\n" \
"fstpl (%esp)" );\
esp += 8;\
break;\
case 's':\
asm volatile("push %0" ::\
"r"(&argv[i][1])) ;\
esp += 4;\
break;\
default:\
printf("error argument type");\
goto exit_runso;\
}\
}
#define RESTORE_STACK\
asm volatile("add %0, %%esp" :: "r"(esp))
int main(int argc, char *argv[])
{
void* handle;
char* error;
int i;
int esp = 0;
void* func;
handle = dlopen(argv[1], RTLD_NOW);
if (handle == 0) {
printf("Can't find library: %s\n", argv[1]);
}
func = dlsym(handle, argv[2]);
if ( (error = dlerror()) != NULL ) {
printf("Find symbol %s error: %s\n", argv[2], error);
}
switch (argv[argc-1][0])
{
case 'i':
{
typedef int(*f)();
f func_int = (f)func;
SETUP_STACK;
int ret = func_int();
RESTORE_STACK;
printf("ret = %d\n", ret);
break;
}
case 'd':
{
typedef double(*f)();
f func_double = (f)func;
SETUP_STACK;
double ret = func_double();
RESTORE_STACK;
printf("ret = %f\n", ret);
break;
}
case 's':
{
typedef char*(*f)();
f func_str = (f)func;
SETUP_STACK;
char* ret = func_str();
RESTORE_STACK;
printf("ret = %s\n", ret);
break;
}
case 'v':
{
typedef void(*f)();
f func_void = (f)func;
SETUP_STACK;
func_void();
RESTORE_STACK;
printf("ret = void\n");
break;
}
} // end of switch
exit_runso:
dlclose(handle);
}
Linux共享库的组织
共享库命名规则:
libname.so.x.y.z
x: 主版本号,y: 次版本号,z: 发布版本号
主版本号: 重大的不兼容升级,个版本之间不兼容
次版本号: 库的增量升级,增加了一些新的接口符号,且保持原来的符号不变
**发布版本号:**表示库的一些错误修正、性能改进等
LD_LIBRARY_PATH=/home/user /bin/ls
另一种方式
/lib/ld-linux.so.2 -library-path /home/user /bin/ls
动态连接器找查共享库的顺序:
- 由环境变量LD_LIBRARY_PATH指定的路径
- 由路径缓存文件/etc/ld.so.cache指定的路径
- 默认共享库目录,先/usr/lib,然后/lib
# 生共享库,并指定soname
gcc -c -g -Wall -o libfoo1.o libfoo1.c
gcc -c -g -Wall -o libfoo2.o libfoo2.c
gcc -shared -fPIC -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0.0 \
libfoo1.o libfoo2.o -lbar -lbar2
ld -rpath /home/mylib -o program.out program.o -lsomelib
这样产生的输出可执行文件program.out在被动态连接器装载时,动态连接器会首先在"/home/mylib"找查共享库
strip libfoo.so # 清除掉共享库或可执行文件的所有符号和调试信息
ldconfig -n shared_library_directory
共享库的构造和析构函数
在函数声明时加上“attribute((constructor))”的属性,即指定该函数为共享库构造函数,拥有这种属性的函数会在共享库加载时被执行,即在main函数之前执行。如果使用dlopen()打开共享库,共享库构造函数会在dlopen()返回之前被执行。
“__attribute((destructor))”析构函数,在main函数执行完毕之后执行(或程序调用exit()时执行)如果共享库时运行时加载的,析构函数会在dlclose()返回之前执行。
void __attribute__((constructor(数字越小优先级越高))) init_function(void);
void __attribute__((destructor(与构造相反))) fini_function(void);
__attribute__语法是GCC对c/c++语言的扩展,在其他编译器上这种语法并不通用
动态链接堆栈初始化
#include <stdio.h>
#include <stdint.h>
#include <elf.h>
int main(int argc, char* argv[])
{
printf("addr argc: %x\n", &argc);
uintptr_t* p = (uintptr_t*)argv;
printf("p-1: %x\n", p-1);
printf("argument number: %d\n", *(int*)(p-1) );
printf("\narguments:\n");
char** tmp = argv;
while (*tmp)
{
printf("%s\n", *tmp);
++tmp;
}
p += argc;
++p;
printf("\nenv info:\n");
char** tmp2 = (char**)p;
while (*tmp2)
{
printf("%s\n", *tmp2);
// ++p;
++tmp2;
// tmp2 = (int*)p;
}
p = (uintptr_t*)tmp2;
++p;
printf("\nAuxiliary Vectors::\n");
Elf64_auxv_t* aux = (Elf64_auxv_t*)p;
while (aux->a_type != AT_NULL)
{
printf("Type: %02d Value: %x\n", aux->a_type, aux->a_un.a_val);
++aux;
}
}
函数调用
一个C语言运行库大致包含:
- 启动与退出:包括入口函数及入口函数所依赖的其他函数
- 标准函数:由C语言标准规定的函数
- I/O:I/O功能的封装和实现
- 堆:堆的封装和实现
- 语言实现:语言中一些特殊功能的实现
- 调试:实现调试功能的代码
C++全局构造与析构
“.init”和“.finit”段的代码最终会被拼成_init()和_finit()函数
void my_init(void)
{
printf("hello\n");
}
typedef void(*ctor_t)();
// 在.ctors段里添加一个函数指针
ctor_t __attribute__((section(".ctors"))) my_init_p = &my_init;
或者:
void my_init(void) __attribute__((constructor));
void my_init(void)
{
printf("hello\n");
}
#pragma section(".CRT$XCA", long, read)
#pragma section(".CRT$XCZ", long, read)
#define _CRTALLOC(x) __declspec(allocate(x))
其后的变量将被分配在段x里
#pragma section("section-name" [, attributes])
生成名为"section-name"的段并具有attributes属性
模拟实现库函数 fread
int fflush(FILE* stream);
int setvbuf(FILE* stream, char* buf, int mode, size_t size);
mode: _IONBF 无缓冲
_IOLBF 行缓冲,仅用于文本文件,遇到换行就输出
_IOFBF 仅当缓冲满时才进行flush
void setbuf(FILE* stream, char* buf);
== 设置文件缓冲 setvbuf(stream buf, _IOFBF, BUFSIZ);
syscall
Linux使用0x80号中断作为系统调用的入口
Windows使用0x2E号中断作为系统调用入口
x86下,Linux系统调用由0x80中断完成,各个通用寄存器用于传递参数,EAX寄存器用于表示系统调用的接口号,比如EAX = 1表示退出进程(exit);EAX=2表示创建进程(fork);EAX=3表示读取文件或IO(read);EAX=4表示写文件或IO(write),每个系统调用都对应与内核源代码中的一个函数,它们都以“sys_”开头,比如exit调用对应内核中的sys_exit函数。当系统调用返回时,EAX又作为调用结果的返回值。
这些系统调用的C语言形式在<unistd.h>中
syscall 原理
- cpu每过一段时间去看一看有没有系统调用
- 发生系统调用时向cpu发送个信号,CPU收到后再去处理
将系统调用号放入eax寄存器,然后使用int 0x80调用中断,中断服务程序从eax里取的系统调用号,进而调用对应的函数
基于int的Linux的经典系统调用实现
#define _syscall0(type, name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
__syscall_return(type, __res); \
}
syscall0(pid_t, fork)展开后
pid_t fork(void)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_fork));
__syscall_return(pid_t, __res);
}
易读形式
pid_t fork(void)
{
long __res;
$eax = __NR_fork
int $0x80
__res = $eax
__syscall_return(pid_t, __res);
}
__NR_fork是一个宏,表示fork系统调用的调用号
#define __syscall_return(type, res) \
do { \
if ((unsigned long) res >= (unsigned long)(-125)) { \
errno = -(res); \
res = -1; \
} \
return (type)(res); \
} while (0)
这个宏用于检查系统调用的返回值,并把它相应的转换为C语言的errno错误码
汇编后得到类似代码:
fork:
mov eax, 2
int 0x80
cmp eax, 0xFFFFFF83
jb syscall_noerror
neg eax
mov errno, eax
mov eax, 0xFFFFFFFF
syscall_noerror:
ret
当用户调用某个系统调用的时候,实际是执行了以上一段汇编代码。CPU执行到 int $0x80 时,会保存现场以便恢复,接着会将特权状态切换到内核态。然后CPU便会找查中断向量表中的第0x80号元素。
在实际执行中断向量表中的第0x80号元素所对应的函数之前,CPU首先还要进行栈的切换。在Linux中,用户态和内核态使用的是不同的栈,两者各自负责各自的函数调用,互不干扰。但在应用程序调用0x80号中断时,程序的执行流程从用户态切换到内核态,这时程序的当前栈必须也相应地从用户态切换到内核态。从中断处理函数中返回时,程序的当前栈还要从内核栈切换回用户栈。
“当前栈”,指的是ESP的值所在的栈空间。如果ESP的值位于用户栈的范围内,那么程序的当前栈就是用户栈,反之亦然。此外,寄存器SS的值还应该指向当前栈所在的页。将当前栈由用户栈切换为内核栈的实际行为就是:
- (1)保存当前ESP, SS的值
- (2)将ESP, SS的值设置为内核栈的相应值
反回来,内核态切换为用户态: - (1)恢复原来的ESP, SS的值
- 用户态的ESP和SS的值保存在内核栈上(每个进程都有自己的内核栈)
- 在内核栈中依次压入用户态的寄存器SS, ESP, EFLAGS, CS, EIP
附录
ELF常见段
段名 | 说明 |
---|---|
.bss | 未初始化的数据,在程序启动时,在内存中会被清零。该段不占用磁盘空间 |
.comment | 包含编译器版本信息 |
.data | 已初始化的全局变量、静态变量 |
.debug | 调试信息 |
.dynamic | 动态链接信息 |
.dynstr | 动态链接时的字符串表,主要时动态链接符号的符号名 |
.dynsym | 动态链接符号表 |
.fini | 程序退出时执行的代码,晚于main,多用于实现C++全局析构 |
.init | 程序执行前的初始化代码,早与main |
.interp | 包含了动态连接器的路径 |
.rodata | 制度数据段 |
.shstrtab | 段名字符串表 |
.strtab | 字符串表,通常时符号表里的符号名所需要的的字符串 |
.symtab | 符号表 |
.tbss | 线程局部存储的未初始化数据 |
.tdata | 线程局部存储的初始化数据 |
.text | 代码段 |
.ctors | 全局构造函数指针 |
.dtors | 全局析构函数指针 |
.got.plt | PLT信息 |
.jcr | Java程序相关 |
gcc, GCC编译器
参数 | |
---|---|
-E | 只进行预处理 |
-c | 只编译不链接 |
-o | 指定输出的文件名 |
-S | 输出编译后的汇编代码 |
-I | 指定头文件路径 |
-e name | 指定name为程序入口地址 |
-ffreestanding | 编译独立的程序,不会自动链接C运行库、启动文件等 |
-finline-functions,-fno-inline-functions | 启用/关闭内敛函数 |
-g | 加入调试信息 |
-L | 指定链接时找查路径,多个路径用冒号隔开 |
-nostartfiles | 不要链接启动文件,比如crtbegin.o, crtend.o |
-nostdlib | 不要链接标准库文件,主要时C运行库 |
-O0 | 关闭所有优化选项 |
-shared | 产生共享对象文件 |
-static | 使用静态链接 |
-Wall | 启用编译警告 |
-fPIC | 使用地址无关代码模式进行编译 |
-fPIE | 使用地址无关代码模式编译可执行文件 |
-XLinker <option> | 把option传递给链接器 |
-fomit-frame-pointer | 禁止使用EBP作为函数帧指针 |
-fno-builtin | 禁止GCC编译器内置函数 |
-fno-stack-protector | 关闭堆栈保护 |
-ffunction-sections | 将每个函数编译到独立的代码段 |
-fdata-sections | 将全局/静态变量编译到独立的数据段 |
ld, GNU连接器
-static | 静态链接 |
-l<libname> | 指定链接某个库 |
-e name | 指定name为程序入口 |
-r | 合并目标文件,不进行最终链接 |
-L<dir> | 指定链接找查路径,多个路径用冒号隔开 |
-M | 将连接时的符号和地址输出成一个映射文件 |
-o | 指定输出文件名 |
-s | 清楚输出文件中的符号信息 |
-S | 清楚输出文件中的调试信息 |
-T<scriptfile> | 指定链接脚本 |
-version-script <file> | 指定符号版本脚本文件 |
-soname <name> | 指定输出共享库的SONAME |
-export-dynamic | 将全局符号全部导出 |
-verbose | 链接时输出详细信息 |
-rpath <path> | 指定链接时库找查路径 |
objdump, GNU二进制文件查看器
readelf
-a | 列举.a文件中所有的目标文件 |
-C | 对于c++符号名进行反修饰Demangle |
-g | 显示调试信息 |
-d | 对包含机器执行的段反汇编 |
-D | 对所有的段反汇编 |
-f | 显示目标我呢见文件头 |
-h | 显示段表 |
-l | 显示行号信息 |
-r | 显示重定位信息 |
-S | 希纳是源代码和反汇编代码(包含-d参数) |
-t | 显示符号表 |
-T | 显示动态链接符号表 |
-x | 显示文件的所有文件头 |
线程私有全局变量
对于GCC编译器使用__thread
__thread int a;