系列文章目录
文章目录
- 系列文章目录
- 前言
- 一、
- dlopen, dlerror, dlclose
- 直接执行动态库中的某个函数/某段代码
- Linux共享库的组织
- 共享库的构造和析构函数
- 动态链接堆栈初始化
- C++全局构造与析构
- 模拟实现库函数 fread
- syscall
- syscall 原理
- 基于int的Linux的经典系统调用实现
前言
一、
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