编译、重定位、装入和链接
- 存放文件的基本信息
.text段
- 存放编译好的机器代码
.data段
- 存放已经初始化的全局变量和静态变量
.bss段
- 未初始化的全局变量和静态变量以及初始化为0的全局变量和静态变量
better save space- 并不占据实际的空间,只是一个占位符
- 系统真正运行的时候会分配空间并且初始化为0
COMMON
- 存放未初始化的全局变量
.rodata
- 存放只读数据
- 比如字符串和
switch的跳转表.rel.text
- 存放需要重定位的代码
.rel.data
- 存放已初始化的数据的重定位
可执行文件的结构
ELF Header
- 文件的总体格式
.init
- 初始化用的代码
.text .rodata和.data
- 与可重定位文件类似
强符号和弱符号
- 强符号
- 函数和已经初始化的全局变量
- 弱符号
- 未初始化的全局变量
- 多个同名的强符号一起出现的时候会引起链接器错误
- 强符号和弱符号一起出现的时候会同意认为是强符号,而不会报错
- 类型不同的强弱符号,在弱符号出现的模块中,类型会保留弱符号的类型,尽管实际上内存中存储的是强符号的类型
- 给编译器
-fno-common选项,会在多重定义时报错防止上述错误
- 多个弱符号也会认为是同一个变量
运行时的内存映像
- 代码段,数据段和堆是相邻的
- 栈在最大可用地址的位置,从大到小,堆从小到大
- 程序运行加载的过程
- 只编译不链接就得到可重定位目标文件
一个指令序列加载的例子
// Generated by GPT-4; unmodified
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <binary_file>\n", argv[0]);
return 1;
}
// Open the binary file
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
// Get the file size
off_t file_size = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
// Allocate memory for the binary
void *mem = mmap(NULL, file_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
if (mem == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// Close the file
close(fd);
// Cast the memory address to a function pointer and call it
void (*binary_func)() = (void (*)())mem;
binary_func();
// Clean up
munmap(mem, file_size);
return 0;
} - 也就是打开文件,创建文件和内存位置之间大小等于文件大小的映射关系(mmap),将指定大小的文件映射到内存空间中,然后将映射到的内存地址强制类型转换为函数指针,然后从这个位置开始执行
一个应用程序加载的例子
- 应用程序在内存中除了代码段
.text,数据段.data和BSS段.bss以外,还包含动态分配内存用的堆和栈空间
void execve_(const char *file, char *argv[], char *envp[]) {
// WARNING: This execve_ does not free process resources.
// **NOT** all process states are properly initialized.
int fd = open(file, O_RDONLY);
assert(fd > 0);
// Map ELF header to memory
Elf64_Ehdr *h = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
assert(h != MAP_FAILED);
assert(h->e_type == ET_EXEC && h->e_machine == EM_X86_64);
Elf64_Phdr *pht = (Elf64_Phdr *)((char *)h + h->e_phoff);
for (int i = 0; i < h->e_phnum; i++) {
Elf64_Phdr *p = &pht[i];
if (p->p_type == PT_LOAD) {
// Memory map region
uintptr_t map_beg = ROUND(p->p_vaddr, p->p_align);
uintptr_t map_end = map_beg + p->p_memsz;
while (map_end % p->p_align != 0) map_end++;
// Memory map flags
int prot = 0;
if (p->p_flags & PF_R) prot |= PROT_READ;
if (p->p_flags & PF_W) prot |= PROT_WRITE;
if (p->p_flags & PF_X) prot |= PROT_EXEC;
// Memory map size
int map_sz = p->p_filesz + (p->p_vaddr % p->p_align);
while (map_sz % p->p_align != 0) map_sz++;
// Map file contents to memory
void *ret = mmap(
(void *)map_beg, // addr, rounded to ALIGN
map_sz, // length
prot, // protection
MAP_PRIVATE | MAP_FIXED, // flags, private & strict
fd, // file descriptor
ROUND(p->p_offset, p->p_align) // offset
);
assert(ret != MAP_FAILED);
// Map extra anonymous memory (e.g., bss)
intptr_t extra_sz = p->p_memsz - p->p_filesz;
if (extra_sz > 0) {
uintptr_t extra_beg = map_beg + map_sz;
ret = mmap(
(void *)extra_beg, extra_sz, prot, // addr, length, protection
MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED, // flags
-1, 0 // no file
);
assert(ret != MAP_FAILED);
}
}
}
close(fd);
static char stack[STK_SZ], rnd[16];
void *sp = (void *)ROUND(stack + sizeof(stack) - 4096, 16);
void *sp_exec = sp;
int argc = 0;
// argc
while (argv[argc]) argc++;
push(sp, intptr_t, argc);
// argv[], NULL-terminate
for (int i = 0; i <= argc; i++)
push(sp, intptr_t, argv[i]);
// envp[], NULL-terminate
for (; *envp; envp++) {
if (!strchr(*envp, '_')) // remove some verbose ones
push(sp, intptr_t, *envp);
}
// auxv[], AT_NULL-terminate
push(sp, intptr_t, 0);
push(sp, Elf64_auxv_t, { .a_type = AT_RANDOM, .a_un.a_val = (uintptr_t)rnd } );
push(sp, Elf64_auxv_t, { .a_type = AT_NULL } );
asm volatile(
"mov $0, %%rdx;" // required by ABI
"mov %0, %%rsp;"
"jmp *%1" : : "a"(sp_exec), "b"(h->e_entry));
}
int main(int argc, char *argv[], char *envp[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s file [args...]\n", argv[0]);
exit(1);
}
execve_(argv[1], argv + 1, envp);
} - 手动实现了一个应用程序的加载器
- 先加载了ELF的文件头(header)到内存中
- ELF文件中有一些
LOAD标记,指示文件中的某个位置的规定大小的部分需要加载到内存中指定的位置上,还原程序执行的初始状态 - 然后给程序准备初始状态下的栈中的内容
flag可以设置为RTLD_LASY,将代码推迟到执行的时候再符号解析- 还提供了一个获取共享库的符号地址的方法
void* dlsym(void* handle, char* symbol);
- 其中的
handle就是前面打开的共享库 - symbol就是其中的符号名称
int dlclose(void* handle);
- 卸载共享库
中断
- 异步中断
- 不是当前程序引起的,比如I/O操作引起的
- 同步中断
- CPU中执行的指令引起的中断
程序异常
- 所谓的段错误(Segmentation Fault)就是程序试图访问一块未申请的内存空间引起的,也就是图中的13号
- 18号异常时机器检查,一般是硬件错误引起的
fork调用一次返回两次- 父进程和子进程各一次
execve调用一次不返回程序执行的传参
argv
- argv的第一个是可执行文件的名字
argc是参数的数量
- 终止运行但是没有被父进程回收的进程就是
zombie进程回收进程
pid_t waitpid(pid_t pid,int *statusp,int options);
- 如果pid>0,则回收该pid指定的某个确定的进程
- 如果pid=-1,则回收该进程创建的所有子进程
- 第二个参数是导致返回的子进程的状态信息(终止的原因是正常返回还是异常终止还是其他)
waitpid程序不会按照任何特定顺序回收子进程,除非明确指定发送信号
int kill(pid_t pid,int sig);
- 如果pid为0,则发送信号给调用进程所在进程组中的所有进程,包括自己
- 如果pid>0,则发送信号给pid指定的进程
- 如果pid<0,则发送信号给进程组-pid的每个进程
unsigned int alarm(unsigned int secs);
- 进程可以用
alarm给自己发送信号,参数是在 secs秒之后给调用进程发送一个SIGALRM - secs为0就不会调用新的闹钟了
信号处理
- 一个进程最多只有一个同类型的信号,等待处理的信号超过一个的时候会被直接丢弃
- 处理多个信号