操作系统相关杂项

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、
      • dlopen, dlerror, dlclose
      • 直接执行动态库中的某个函数/某段代码
  • Linux共享库的组织
      • 共享库的构造和析构函数
  • 动态链接堆栈初始化
  • C++全局构造与析构
  • 模拟实现库函数 fread
  • syscall
    • syscall 原理
    • 基于int的Linux的经典系统调用实现
  • 附录
    • ELF常见段
    • gcc, GCC编译器
    • ld, GNU连接器
    • objdump, GNU二进制文件查看器


前言


一、

dlopen, dlerror, dlclose

#include 
#include 

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 
#include 
#include 

#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: 发布版本号
主版本号: 重大的不兼容升级,个版本之间不兼容
次版本号: 库的增量升级,增加了一些新的接口符号,且保持原来的符号不变
**发布版本号:**表示库的一些错误修正、性能改进等

操作系统相关杂项_第1张图片
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 
#include 
#include 

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;
    }
    
}

函数调用
操作系统相关杂项_第2张图片

一个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又作为调用结果的返回值。

在这里插入图片描述
操作系统相关杂项_第3张图片

这些系统调用的C语言形式在

syscall 原理

  1. cpu每过一段时间去看一看有没有系统调用
  2. 发生系统调用时向cpu发送个信号,CPU收到后再去处理

将系统调用号放入eax寄存器,然后使用int 0x80调用中断,中断服务程序从eax里取的系统调用号,进而调用对应的函数

基于int的Linux的经典系统调用实现

操作系统相关杂项_第4张图片

#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

操作系统相关杂项_第5张图片

中断时用户栈和内核栈切换

操作系统相关杂项_第6张图片

Linux i386中断服务程序

操作系统相关杂项_第7张图片

Linux 系统调用流程

附录

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传递给链接器
-fomit-frame-pointer 禁止使用EBP作为函数帧指针
-fno-builtin 禁止GCC编译器内置函数
-fno-stack-protector 关闭堆栈保护
-ffunction-sections 将每个函数编译到独立的代码段
-fdata-sections 将全局/静态变量编译到独立的数据段

ld, GNU连接器

-static 静态链接
-l 指定链接某个库
-e name 指定name为程序入口
-r 合并目标文件,不进行最终链接
-L 指定链接找查路径,多个路径用冒号隔开
-M 将连接时的符号和地址输出成一个映射文件
-o 指定输出文件名
-s 清楚输出文件中的符号信息
-S 清楚输出文件中的调试信息
-T 指定链接脚本
-version-script 指定符号版本脚本文件
-soname 指定输出共享库的SONAME
-export-dynamic 将全局符号全部导出
-verbose 链接时输出详细信息
-rpath 指定链接时库找查路径

objdump, GNU二进制文件查看器

readelf

-a 列举.a文件中所有的目标文件
-C 对于c++符号名进行反修饰Demangle
-g 显示调试信息
-d 对包含机器执行的段反汇编
-D 对所有的段反汇编
-f 显示目标我呢见文件头
-h 显示段表
-l 显示行号信息
-r 显示重定位信息
-S 希纳是源代码和反汇编代码(包含-d参数)
-t 显示符号表
-T 显示动态链接符号表
-x 显示文件的所有文件头

你可能感兴趣的:(Linux,操作系统,操作系统,linux)