内容简介 · · · · · ·
这本书主要介绍系统软件的运行机制和原理,涉及在Windows和Linux两个系统平台上,一个应用程序在编译、链接和运行时刻所发生的各种事项,包括:代码指令是如何保存的,库文件如何与应用程序代码静态链接,应用程序如何被装载到内存中并开始运行,动态链接如何实现,C/C++运行库的工作原理,以及操作系统提供的系统服务是如何被调用的。每个技术专题都配备了大量图、表和代码实例,力求将复杂的机制以简洁的形式表达出来。本书最后还提供了一个小巧且跨平台的C/C++运行库MiniCRT,综合展示了与运行库相关的各种技术。
对装载、链接和库进行了深入浅出的剖析,并且辅以大量的例子和图表,可以作为计算机软件专业和其他相关专业大学本科高年级学生深入学习系统软件的参考书。同时,还可作为各行业从事软件开发的工程师、研究人员以及其他对系统软件实现机制和技术感兴趣者的自学教材。
目录 · · · · · ·
第1章 温故而知新
1.1 从HELLO WORLD 说起
1.2 万变不离其宗
1.3 站得高,望得远
1.4 操作系统做什么
1.5 内存不够怎么办
1.6 众人拾柴火焰高
1.7 本章小结
第2部分 静态链接
第2章 编译和链接
2.1 被隐藏了的过程
2.2 编译器做了什么
2.3 链接器年龄比编译器长
2.4 模块拼装——静态链接
2.5 本章小结
第3章 目标文件里有什么
3.1 目标文件的格式
3.2 目标文件是什么样的
3.3 挖掘SIMPLESECTION.O
3.4 ELF 文件结构描述
3.5 链接的接口——符号
3.6 调试信息
3.7 本章小结
第4章 静态链接
4.1 空间与地址分配
4.2 符号解析与重定位
4.3 COMMON 块
4.4 C++相关问题
4.5 静态库链接
4.6 链接过程控制
4.7 BFD 库
4.8 本章小结
第5章 WINDOWS PE/COFF
5.1 WINDOWS 的二进制文件格式PE/COFF 134
5.2 PE 的前身——COFF
5.3 链接指示信息
5.4 调试信息
5.5 大家都有符号表
5.6 WINDOWS 下的ELF——PE
5.7 本章小结
第3部分 装载与动态链接
第6章 可执行文件的装载与进程
6.1 进程虚拟地址空间
6.2 装载的方式
6.3 从操作系统角度看可执行文件的装载
6.4 进程虚存空间分布
6.5 LINUX 内核装载ELF 过程简介
6.6 WINDOWS PE 的装载
6.7 本章小结
第7章 动态链接
7.1 为什么要动态链接
7.2 简单的动态链接例子
7.3 地址无关代码
7.4 延迟绑定(PLT)
7.5 动态链接相关结构
7.6 动态链接的步骤和实现
7.7 显式运行时链接
7.8 本章小结
第8章 LINUX 共享库的组织
8.1 共享库版本
8.2 符号版本
8.3 共享库系统路径
8.4 共享库查找过程
8.5 环境变量
8.6 共享库的创建和安装
8.7 本章小结
第9章 WINDOWS 下的动态链接
9.1 DLL 简介
9.2 符号导出导入表
9.3 DLL 优化
9.4 C++与动态链接
9.5 DLL HELL
9.6 本章小结
第4部分 库与运行库
第10章 内存
10.1 程序的内存布局
10.2 栈与调用惯例
10.3 堆与内存管理
10.4 本章小结
第11章 运行库
11.1 入口函数和程序初始化
11.2 C/C++运行库
11.3 运行库与多线程
11.4 C++全局构造与析构
11.5 FREAD 实现
11.6 本章小结
第12章 系统调用与API
12.1 系统调用介绍
12.2 系统调用原理
12.3 WINDOWS API
12.4 本章小结
第13章 运行库实现
13.1 C 语言运行库
13.2 如何使用MINI CRT
13.3 C++运行库实现
13.4 如何使用MINI CRT++
13.5 本章小结
附录A
A.1 字节序(BYTE ORDER)
A.2 ELF 常见段
A.3 常用开发工具命令行参考
索引
· · · · · · (收起)
喜欢读"程序员的自我修养"的人也喜欢的电子书 · · · · · ·
喜欢读"程序员的自我修养"的人也喜欢 · · · · · ·
程序员的自我修养的话题 · · · · · · ( 全部 条 )



程序员的自我修养的书评 · · · · · · ( 全部 78 条 )
> 更多书评78篇
-
ziyoudefeng (娜娜,有你生活真幸福~~)
本篇笔记全文摘自后面参考文献列出的地址,不过原文中有一些错别字,我这里把错别字改了改,然后也是按照自己机器上代码的结果来进行演示。再结合本书前面第6章“6.5 Linux内核装载ELF过程简介”来进行说明。 1、开始 The question is simple: how does linux execute my main()? Through this document, I'll use the following simple C program to illustrate how it works. It's called "execute_main.c" ...2012-12-19 20:41 1人喜欢
本篇笔记全文摘自后面参考文献列出的地址,不过原文中有一些错别字,我这里把错别字改了改,然后也是按照自己机器上代码的结果来进行演示。再结合本书前面第6章“6.5 Linux内核装载ELF过程简介”来进行说明。1、开始 The question is simple: how does linux execute my main()? Through this document, I'll use the following simple C program to illustrate how it works. It's called "execute_main.c"int main() { return 0; }
2、编译 gcc -o execute_main execute_main.c3、What's in the executable?i4@i4:~/文档/tempfile$ objdump -f execute_main execute_main: file format elf32-i386 architecture: i386, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x08048300
4、What's at address "0x08048300", that is, starting address?i4@i4:~/文档/tempfile$ objdump -d execute_main
The output is a little bit long so I'll not paste all the output from objdump. Our intention is see what's at address 0x08048300. Here is the output.Disassembly of section .text: 08048300 <_start>: 8048300: 31 ed xor %ebp,%ebp 8048302: 5e pop %esi 8048303: 89 e1 mov %esp,%ecx 8048305: 83 e4 f0 and $0xfffffff0,%esp 8048308: 50 push %eax 8048309: 54 push %esp 804830a: 52 push %edx 804830b: 68 30 84 04 08 push $0x8048430 8048310: 68 c0 83 04 08 push $0x80483c0 8048315: 51 push %ecx 8048316: 56 push %esi 8048317: 68 b4 83 04 08 push $0x80483b4 804831c: e8 cf ff ff ff call 80482f0 <__libc_start_main@plt> 8048321: f4 hlt
Looks like some kind of starting routine called "_start" is at the starting address. What it does is clear a register, push some values into stack and call a function. According to this instruction, the stack frame should look like this. ---------------- %eax ---------------- %esp ---------------- %edx ---------------- 0x8048430 ---------------- 0x80483c0 ---------------- %ecx ---------------- %esi ----------------Stack Top 0x80483b4 ---------------- Now, as you already wonder,we've got a few questions regarding this stack frame. Q1: What are those hex values about? Q2: What's at address 80482f0, which is called by _start? (PS: 即上面反汇编代码中804831c: e8 cf ff ff ff call 80482f0 <__libc_start_main@plt>) Q3: Looks like the assembly instructions doesn't initialize any register with possibly meaningful values. Then who initializes the registers? (PS: 个人认为这个很重要,后面有较多篇幅来描述) A1: The hexa values. If you look at disassembled output from objdump carefully, you can answer this question easily.再把反汇编代码摘出来,然后给出答案:08048430 <__libc_csu_fini>: ...... 080483c0 <__libc_csu_init>: ...... 080483b4 <main>: ......
Right now, let's not care about these stuffs. And basically, all those hexa values are function pointers. A2: What's at address 80482f0?080482f0 <__libc_start_main@plt>: 80482f0: ff 25 04 a0 04 08 jmp *0x804a004 ......
Here *0x804a004 is a pointer operation. It just jumps to an address stored at address 0x804a004.那么这里地址0x804a004处是什么呢?我们看下面代码(PS: 原文这里又说了说关于动态链接的内容,这些内容在书第7章已经讲了,我这里不再陈述。大家跟着看下代码就明白了。)i4@i4:~/文档/tempfile$ objdump -R execute_main execute_main: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049ff0 R_386_GLOB_DAT __gmon_start__ 0804a000 R_386_JUMP_SLOT __gmon_start__ 0804a004 R_386_JUMP_SLOT __libc_start_main
看到没,上面0x804a004其实是__libc_start_main函数的地址,是动态链接里面动态重定位里的一种类型。即,其实我们是想调用__libc_start_main函数。5、What's __libc_start_main? Now the ball is on libc's hand. __libc_start_main is a function in libc.so.6. If you look for __libc_start_main in glibc source code, the prototype looks like this.int __libc_start_main( int (*main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end) );
总结: 到此我们知道: all the assembly instructions do is set up argument stack and call _libc_start_main. What this function does is setup/initialize some data structures/environments and call our main(). Let's look at the stack frame with this function prototype. ---------------- %eax this is 0 ---------------- %esp stack_end ---------------- %edx _rtlf_fini ---------------- 0x8048430 _libc_csu_fini ---------------- 0x80483c0 _libc_csu_init ---------------- %ecx argv ---------------- %esi argc ----------------Stack Top 0x80483b4 main ---------------- According to this stack frame, esi, ecx, edx, esp, eax registers should be filled with appropriate values before __libc_start_main() is executed. And clearly this registers are not set by the startup assembly instructions shown before. Then, who sets these registers? Now I guess the only thing left. The kernel. Now let's go back to our third question. A3: What does the kernel do? When we execute a program by entering a name on shell, this is what happens on Linux.(PS: 下面的描述太长,其实描述的也就是书里第6章“6.5 Linux内核装载ELF过程简介”的内容。我在这段英文后面给出大概的执行流程) 1: The shell calls the kernel system call "execve" with argc/argv. The kernel system call handler gets control and start handling the system call. In kernel code, the handler is "sys_execve". On x86, the user-mode application passes all required parameters to kernel with the following registers. %ebx : pointer to program name string %ecx : argv array pointer %edx : environment variable array pointer. 2: The generic execve kernel system call handler, which is do_execve, is called. What it does is set up a data structure and copy some data from user space to kernel space and finally calls search_binary_handler(). Linux can support more than one executable file format such as a.out and ELF at the same time. For this functionality, there is a data structure "struct linux_binfmt", which has a function pointer for each binary format loader. And search_binary_handler() just looks up an appropriate handler and calls it. In our case, load_elf_binary() is the handler. To explain each detail of the function would be lengthy/boring work. So I'll not do that. If you are interested in it, read a book about it. As a picture tells a thousand words, a thousand lines of source code tells ten thousand words (sometimes). Here is the bottom line of the function. It first sets up kernel data structures for file operation to read the ELF executable image in. Then it sets up a kernel data structure: code size, data segment start, stack segment start, etc. And it allocates user mode pages for this process and copies the argv and environment variables to those allocated page addresses. Finally, argc, the argv pointer, and the envrioronment variable array pointer are pushed to user mode stack by create_elf_tables(), and start_thread() starts the process execution rolling. 这两段描述的大概流程是:shell ---> execve (同时传递argc, argv) ---> sys_execve (用户模式下的一些内容传递给内核模式里的一些寄存器,即%ebx, %ecx, %edx。这三个的具体内容上面1:里有) ---> search_binary_handler() ---> load_elf_binary()。最后这个load_elf_binary()设置了不少内容,设置完之后就是返回到用户模式下运行e_entry的入口函数。 在入口函数_start()执行之前,栈的内容如下: ---------------- env pointer ---------------- argv pointer ----------------Stack Top argc ---------------- And the assembly instructions gets all information from stack bypop %esi <--- get argc mov %esp, %ecx <--- get argv (actually the argv address is the same as the current stack pointer.)
And now we are all set to start executing.6、What about the other registers? For %esp, this is used for stack end in application program. After popping all necessary information, the _start rountine simply adjusts the stack pointer (%esp) by turning off lower 4 bits from esp register. This perfectly makes sense since actually, to our main program, that is the end of stack. For edx, which is used for rtld_fini, a kind of application destructor, the kernel just sets it to 0 with the following macro.#define ELF_PLAT_INIT(_r) do { \ _r->ebx = 0; _r->ecx = 0; _r->edx = 0; \ _r->esi = 0; _r->edi = 0; _r->ebp = 0; \ _r->eax = 0; \} while (0) The 0 means we don't use that functionality on x86 linux.7、Summing up Here is what happens. 1: GCC build your program with crtbegin.o/crtend.o/gcrt1.o And the other default libraries are dynamically linked by default. Starting address of the executable is set to that of _start. 2: Kernel loads the executable and setup text/data/bss/stack, especially, kernel allocate page(s) for arguments and environment variables and pushes all necessary information on stack. 3: Control is pased to _start. _start gets all information from stack setup by kernel, sets up argument stack for __libc_start_main, and calls it. 4: __libc_start_main initializes necessary stuffs, especially C library(such as malloc) and thread environment and calls our main. 5: our main is called with main(argv, argv) Actually, here one interesting point is the signature of main. __libc_start_main thinks main's signature as main(int, char **, char **) If you are curious, try the following prgram.main(int argc, char** argv, char** env) { int i = 0; while(env[i] != 0) { printf("%s\n", env[i++]); } return(0); }
8、Conclusion On Linux, our C main() function is executed by the cooperative work of GCC, libc and Linux's binary loader.9、Reference [1] http://linuxgazette.net/issue84/hawk.html [2] http://www.fsl.cs.sunysb.edu/~siddhi/ELF.pdf回应 2012-12-19 20:41 -
prife (相濡以沫,不如相忘于江湖)
实际上在动态链接器的自举代码中,除了不可以使用全局变量和静态变量之外,甚至不能调用函数,即动态链接器本身的函数也不能调用。这是为什么呢?其实我们在前面分析地址无关代码时已经提到过,实际上使用PIC模式编译的共享对象,对于模块内部的函数调用也是采用外部函数调用一样的方式,即使用GOT/PLT的方式,所以在GOT/PLT没有被重定位之前,自举代码不可以使用任何全局变量,也不可以调用函数。 笔者的疑问主要集中在,.. (6回应)2013-02-05 00:26 1人喜欢
笔者的疑问主要集中在,为什么动态链接器在自举过程中无法调用其本身定义的函数? 作者说他们在地址无关代码章节(7.3节,P188-P199),给出了解释。但是我实在是没找到为什么不行。调用模块内定义的函数,这属于P196 表7-1中的类型1,即模块内部的指令跳转及引用,只需要使用相对跳转和调用即可实现PIC。192页上半页有比较详细的解释,在192页作者又提及实际上在动态链接器的自举代码中,除了不可以使用全局变量和静态变量之外,甚至不能调用函数,即动态链接器本身的函数也不能调用。这是为什么呢?其实我们在前面分析地址无关代码时已经提到过,实际上使用PIC模式编译的共享对象,对于模块内部的函数调用也是采用外部函数调用一样的方式,即使用GOT/PLT的方式,所以在GOT/PLT没有被重定位之前,自举代码不可以使用任何全局变量,也不可以调用函数。
。实际上,全局符号介入在218页才给出了详细的介绍。在218页,作者得出结论这样看起来第一个模块内部调用或跳转很容易解决,但实际上这种方式还有一定的问题,这里存在一个名作共享对象“全局符号介入”问题。
从上面的引文可以看出,函数内部中定义的非static函数也需要通过GOT/PLT调用,而不是P192页中所说的简单的相对地址调用即可。但是这个地方的确切阐述是在P214页之后的P218页完成的。不得不说,这个地方是相对较为严重的疏漏。另外,P218页 全局符号介入与地址无关代码所以对于bar()函数的调用,编译器只能选择第三种,即当作模块的外部符号处理。 ... 即使用static关键字定义bar函数,这种情况下,编译器要确定bar()函数不会其他模块覆盖,就可以使用第一类的方法,即模块内部调用指令。
“如果采用相对地址调用的话,那么这个相对地址就需要重定位” 这一句意思是说,模块中的函数可能被其他模块中的同名函数覆盖,即全局符号介入,并且当前模块中对此函数调用也需要被修改为对被覆盖的同名函数的调用,这就导致这个函数的地址不确定,如果此函数在多个模块存在,并且由于模块加载地址的变动,则此函数可能的地址就可能有多个,也就是说需要对这个函数的地址重定位,所以类型1中介绍的相对地址跳转不能解决问题,依然需要借助GOT实现PIC方式。这样来看,模块内部定义的全局变量和非static函数(也就是导出函数)其实本质上一样的,都需要借助GOT实现。由于可能存在的全局符号介入的问题,foo函数对于bar的调用不能够采用第一类模块调用的方法,因为一旦bar函数由于全局符号介入被其他模块中的同名函数覆盖,那么foo如果采用相对地址调用的话,那么这个相对地址就需要重定位,这由于共享对象的地址无关性矛盾。所以对于bar()函数的调用,编译器只能选择第三种,即当作模块外部符号处理。
6回应 2013-02-05 00:26 -
ziyoudefeng (娜娜,有你生活真幸福~~)
过度优化的问题自然很多,这里不再描述。只说解决方法: 可以使用volatile关键字来试图阻止过度优化,volatile基本可以做到两件事情: 1、阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。 2、阻止编译器调整操作volatile变量的指令顺序。 但即使这样,还是有问题。 因为即使volatile能够阻止编译器调整指令顺序,也无法阻止CPU动态调度换序。下面举例说明: Singleton模式 /.. (2回应)2012-12-22 14:21 1人喜欢
过度优化的问题自然很多,这里不再描述。只说解决方法:
但即使这样,还是有问题。 因为即使volatile能够阻止编译器调整指令顺序,也无法阻止CPU动态调度换序。下面举例说明:Singleton模式可以使用volatile关键字来试图阻止过度优化,volatile基本可以做到两件事情: 1、阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。 2、阻止编译器调整操作volatile变量的指令顺序。
volatile T* pInst = 0; T *GetInstance() { if (pInst == NULL) { lock(); if (pInst == NULL) pInst = new T; unlock(); } return pInst; }
这段代码的问题是CPU的乱序执行。因为C++里的new操作包含了两个步骤:1、分配内存。2、调用构造函数。所以,pInst = new T包含了三个步骤: (1) 分配内存。 (2) 在内存的位置上调用构造函数。 (3) 将内存的地址赋值给pInst。在这三步中,(2)和(3)的顺序是可以颠倒的。也就是说,完全有可能出现这样的情况:pInst的值已经不是NULL,但对象仍然没有构造完毕。这时候如果出现另外一个对GetInstance的并发调用,此时第一个if内的表达式pInst == NULL 为false,所以这个调用会直接返回尚未构造完全的对象的地址(pInst)以提供给用户使用。那么程序这个时候会不会崩溃就取决于这个类的设计如何了。 解决这一问题,即多线程环境中,CPU的换序问题,可以通过barrier指令来完成。#define barrier() __asm__ volatile("lwsync") volatile T* pInst = 0; T *GetInstance() { if (pInst == NULL) { lock(); if (pInst == NULL) { // 通过临时对象指针和屏障指令,就保证了上面(2)(3)的顺序执行 T *temp = new T; barrier(); pInst = temp; } unlock(); } return pInst; }
OK,完美解决~~2回应 2012-12-22 14:21 -
ziyoudefeng (娜娜,有你生活真幸福~~)
/代码内容已省略/ 例如: /代码内容已省略/ 其中: 第一列是VMA的地址范围 第二列是VMA的权限,r代表可读,w代表可写,x代表可执行,p代表私有,s代表共享 第三列是偏移,表示VMA对应的Segment在映像文件中的偏移 第四列是映像文件所在设备的主设备号和次设备号 第五列是映像文件的节点号 最后一列是映像文件的路径2012-12-09 21:24 1人喜欢
cat /proc/pid/maps
例如:i4@i4:~/文档/proger$ ./p162.elf & [1] 6178 i4@i4:~/文档/proger$ cat /proc/6178/maps 08048000-080ee000 r-xp 00000000 08:07 7875904 /home/i4/文档/proger/p162.elf 080ee000-080f0000 rw-p 000a5000 08:07 7875904 /home/i4/文档/proger/p162.elf 080f0000-080f2000 rw-p 00000000 00:00 0 093ba000-093dc000 rw-p 00000000 00:00 0 [heap] b7764000-b7765000 r-xp 00000000 00:00 0 [vdso] bfde5000-bfe06000 rw-p 00000000 00:00 0 [stack]
其中: 第一列是VMA的地址范围 第二列是VMA的权限,r代表可读,w代表可写,x代表可执行,p代表私有,s代表共享 第三列是偏移,表示VMA对应的Segment在映像文件中的偏移 第四列是映像文件所在设备的主设备号和次设备号 第五列是映像文件的节点号 最后一列是映像文件的路径
回应 2012-12-09 21:24
-
vivi (执子之手,将子拖走)
一、程序的编译过程,目标文件里究竟是什么 从源程序到目标文件的生成过程 最简单的编译命令是gcc helloworld.c,它包含了以下几个步骤: 预处理、编译、汇编、链接,下面分别简介。 预处理:处理#define宏定义、#if #ifdef等条件编译指令、#include预编译指令,删除注释,添加行号和文件名标识,保留所有的#pargma编译器指令,经过预编译后的文件为.i文件。预编译命令为:gcc -E hello.c -o hello.i或者cpp hello.c > ... (2回应)2011-05-06 17:24 5人喜欢
一、程序的编译过程,目标文件里究竟是什么从源程序到目标文件的生成过程最简单的编译命令是gcc helloworld.c,它包含了以下几个步骤:预处理、编译、汇编、链接,下面分别简介。预处理:处理#define宏定义、#if #ifdef等条件编译指令、#include预编译指令,删除注释,添加行号和文件名标识,保留所有的#pargma编译器指令,经过预编译后的文件为.i文件。预编译命令为:gcc -E hello.c -o hello.i或者cpp hello.c > hello.i编译:把预处理完得文件进行一系列的词法分析、语法分析、语意分析及优化后产生的汇编代码文件。编译命令为gcc -S hello.i -o hello.s。现在版本的gcc把预编译和编译两个步骤合并成一个步骤,使用ccl程序来完成,命令为ccl hello.c。也可以使用gcc -S hello.c -o hello.s直接从.c文件生成.s汇编文件。汇编:将汇编代码转变成机器可以执行的指令,每一条汇编语句几乎都对应一条机器指令。命令为:as hello.s -o hello.o或者gcc -c hello.s -o hello.o或者我们最熟悉的gcc -c hello.c -o hello.o链接:当我们的程序模块调用a另一个模块中b的函数(foo())或变量时,在编译的阶段编译器并不知道函数foo的地址,所以暂时把调用foo的指令的目标地址搁置,等待最后链接的时候由连接器去将这些指令的目标地址修正。把目标文件和库一起链接成可执行文件。最常见的库时运行时库。目标文件中的格式目标文件就是源代码编译后但未进行链接的那些中间文件,它和可执行文件的内容和结构其实很相似,所以一般和可执行文件采用同一种格式存储,那就是ELF格式。与ELF格式相对应的是Windows平台下的PE格式,它们都是COFF格式的变种。不光是可执行文件和目标文件按照ELF格式存储,动态链接库(.so)和静态链接库(.a)都按照ELF格式存储。ELF格式的文件可分为以下4类:1 可重定位文件(Relocatable File):这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类,代表是Linux的.o文件2 可执行文件(Executable File):这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名,如/bin/bash文件3 共享目标文件(Shared Object File):这种文件包含了代码和数据,可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将这几个共享目标文件与可执行文件结合,作为进程影响的一部分来运行。4 核心转储文件(Core Dump File):当进程意外终止时,系统可以将该进程的地址空间的内容以终止时的一些其他信息转储带核心转储文件。上面几种文件在file命令下会显示出相应的类型。目标文件里有什么:ELF文件最重要的结构是ELF文件头(ELF Header):ELF文件头里定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段得数量等。三个最重要的段:代码段(.text)、数据段(.data)和只读数据段(.rodata)、BSS段(.bss)可以用objdump -s -x -d hello.o来分析各个段得内容,-d表示反汇编,-s表示把各段内容用16进制打印,-x表示详细数据。 顾名思义,.text段主要存放可执行的代码数据;.data段保存的是已经初始化了的全局变量和静态变量;.rodata段保存只读数据,一般是程序里的只读变量(如const修饰的变量)和字符串常量;bss保存未初始化的全局变量和静态变量。除此之外,还有一些常见的段,如.comment段存放编译器版本信息,比如字符串“GCC:(GNU)4.2.0”.dubug段存放调试信息.dynamic存放动态链接信息.hash段存放符号哈希表.line段存放调试时的行号表.note段存放额外的编译器信息.strtab段存放字符串表,用于存储ELF文件中用到的各种字符串,比如符号的名字.shstrtab段存放段表中用到的字符串,最常见的就是段名.symtab段是符号表,通过符号表就能知道这个符号在哪个段,以及在这个段的具体位置,还有这个符号在字符串表中的位置Section Table(段表)也是其中一个段,它保存了各个段的信息,如段名、段的长度、在文件中的偏移、读写权限及段的其他属性.rel.text段是一个重定位表,正如前面所说的,链接器在处理目标文件时,需要对目标文件的某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置,这些重定位的信息都记录在ELF文件的重定位表里通过ELF文件头的信息可以找到段表的位置,从而找到各个段得位置和信息。可以用readelf -S hello.o命令详细查看ELF文件中各段的信息。二、可执行文件的装载程序执行时所需要的指令和数据必须都在内存中才能正常运行,最简单的办法就是将程序运行时所需要的指令和数据全部都装入内存,这样程序就能顺利执行,这就是最简单的静态装入的方法。但是程序所需要的内存数量可能大于物理内存,静态装入就不太现实。 后来研究发现,程序运行时具有局部性原理,所以我们可以将程序最常用的部分驻留在内存中,而将一些不太常用的数据存放在磁盘上,这就是动态装载的原理。 覆盖装入和页映射是两种典型的动态装入方法。覆盖装入就是如果两个模块不会同时运行,则可以使这两个模块共用一块内存,需要哪个模块的时候就装入。现在基本已经淘汰了。 页映射是虚拟存储机制的一部分,它随着虚拟存储的发明而诞生。在页映射机制中,程序装载和操作的单位都是页。最常见的Inter IA32处理器一般都使用4KB大小的页。 假设程序所有的指令和数据总和为32kB,那么程序总共分为8页,将其编号为P0-O7,并假设物理内存只有16kB,编号为F0-F3。如果程序刚开始执行时的入口地址为P0,这时装载管理器发现程序的P0不在内存中,于是将F0分配给P0,并将P0的内容装入F0;运行一段时间后,程序需要用到P5,于是装载管理器将P5装入F1,就这样,当程序用到P3和P6时,它们分别装入到了F2和F3。 很明显,如果这时程序只需要P0,P3,P5和P6这4个页,那么程序就能一直运行下去。但是如果这时候需要访问P4,那么装载管理器需要最初选择,它必须放弃目前正在使用的4个物理内存页中的一个来装载P4。至于选择哪个页,可以有多种算法选择,比如FIFO先入先出算法,LRU最近最少使用算法等。 其实,上面所说的装载管理器就是操作系统的存储管理器。从操作系统的角度看可执行文件的装载 从操作系统的角度看,一个进程最关键的特征就是它拥有独立的虚拟地址空间,这使得它有别于其他进程。这在http://blog.csdn.net/vividonly/archive/2011/05/04/6393516.aspx一文中有详细解释。要使一个可执行程序得以执行,首先必须创建一个进程,然后装载相应的可执行文件并且执行。在有虚拟存储的情况下,上述过程最开始只要做三件事: 1 创建一个独立的虚拟地址空间。这时候并不设置虚拟地址页和物理地址页的映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置。当发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该“缺页”从磁盘中读取到内存中再设置缺页的虚拟页与物理页的映射关系。这都是通过CPU的MMU来实现的。 2 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。这个映射关系在进程中叫做VMA(虚拟内存区域)。比如对于.text段,在创建进程后,会在进程相应的数据结构中设置一个.text段的VMA,记录了它在虚拟空间的地址以及它在ELF文件中的偏移。当程序执行发生页错误,通过查找VMA结构来定位错误页在可执行文件的位置,把可执行文件的那部分内容装入刚分配的物理内存中,进程从刚才页错误的位置重新开始执行。 3 将CPU的指令寄存器设置为可执行文件的入口地址,启动运行。操作系统通过设置CPU的指令寄存器将控制权转交给进程。这里的入口地址就是ELF文件头中的入口地址。2回应 2011-05-06 17:24 -
邻水风竹 (惯看秋月春风)
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,大部分的中间层都是为了封装变化点 linux三个进程创建函数的区别 fork 复制当前进程 exec 使用新的可执行映像覆盖当前可执行映像 clone 创建子进程并从指定位置开始执行 几种锁机制 互斥量:获取互斥量的线程负责释放互斥量,且对系统中任何进程可见 信号量:信号量可由某个线程获取,由另外的线程释放,且对系统中任何进程可见 临界区:可看作只限于..2011-12-08 13:25
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,大部分的中间层都是为了封装变化点linux三个进程创建函数的区别fork 复制当前进程exec 使用新的可执行映像覆盖当前可执行映像clone 创建子进程并从指定位置开始执行几种锁机制互斥量:获取互斥量的线程负责释放互斥量,且对系统中任何进程可见信号量:信号量可由某个线程获取,由另外的线程释放,且对系统中任何进程可见临界区:可看作只限于进程内的互斥量volatile 扩展,可防止编译器过度优化后指令执行顺序改变,导致程序异常,volatile 扩展可保证的两件事情1、阻止编译器为了提高速度将一个变量缓存到寄存器而不写回内存2、阻止编译器调整操作volatile变量的指令顺序内存屏障barrier()可阻止CPU将barrier前后的指令执行顺序交换,以解决乱序执行所带来的问题readelf:查看elf文件的工具editbin可更改DLL基地址修饰后符号名称解析binutils中的c++filt可用来解析被gcc修饰过的符号名称Windows API “UnDecorateSymbolName”可解析被Visual C++编译器的名称修饰规则修饰的符号C++ 代码会自动定义一个宏__cplusplus,可用来解决可能在 C 或 C++ 代码中使用的C库头文件的兼容问题,C++引用C库函数,须在函数声明前加extern "C"强符号:函数和初始化了的全局变量弱符号:未初始化的全局变量__attribute__(weak)将某符号强制性的变为弱符号强符号不能被重定义,但允许存在多个与其同名的弱符号。多个同名的弱符号可以并存,引用时实际使用强符号或占用空间最大的弱符号强引用必须有定义弱引用可没有定义,为定义的弱引用默认值为0,或一个特殊值ABI:指与符号修饰标准、变量内存布局、函数调用方式等跟可执行代码二进制兼容性相关的内容VC 编译预处理指令#pragma data_seg("foo")。。。pragma后定义的变量将全部放入foo段中,除非用另一条pragma预处理指令进行了段切换装载时重定位的动态链接方式,执行速度快,但共享对象无法实现进程间共享地址无关代码,执行速度慢,但可实现进程间共享,地址无关代码一般通过间接跳转的.got段实现动态加载存在同名符号覆盖的情况,此现象称为全局符号介入,因此可能引起程序逻辑错误程序运行中几个比较难发现的逻辑错误1、由于使用了不同版本的编译器,导致ABI不兼容,如不同版本编译器堆栈结构、符号修饰规则、调用习惯、数据结构内存分布、对齐方式等存在差异2、动态链接时发生了未预料的同名符号覆盖3、在多线程程序中使用了非线程安全的函数4、编译器过度优化,更改指令执行顺序(使用volatile解决)5、CPU 乱序执行导致程序异常(使用barrier解决)6、共享库内分配的内存在共享库外释放,或相反的方式,Linux 共享库命名规则:libname.so.x.y.zx:主版本号,表示重大升级,存在不兼容更新y:次版本号,表示增量升级,如添加新的接口功能,为兼容性更新z:分布版本号,表示只有一些错误修正,性能改进等,为兼容性更新libname.so.x又称为共享库的SO-NAMELinux系统为每一类兼容的共享库创建一个以SO-NAME为文件名的软链接命令行中使用的几个可改变动态链接器行为的环境变量添加共享库优先搜索路径LD_LIBRARY_PATH=/home/user cmd预先加载的共享库,可引起链接时同名符号覆盖LD_PRELOAD输出动态链接过程,可以去下列值:files、bindings、libs、versions、reloc、...、allLD_DEBUGgcc编译的共享库,默认情况下没有SO-NAME,可通过-Wl,-soname,myname参数指定DLL导出函数可通过在传统的函数定义前加__declspec(dllexport)来导出,而不用def文件,使用导出函数时在传统声明前加__declspec(dllimport)MSVC中C语言的默认调用规范为_cdecl,不对函数进行符号修饰,__stdcall为windows的通用调用规范,会对符号进行修饰,如:_Sub@16DLL的代码并非地址无关,因此不一定能被多个进程共享,即不一定支持一份代码多进程地址空间映射DLL优化如果知道DLL被加载的位置,使用更改DLL基地址的方法可以免去重定基地址,以提高程序的加载速度使用序号作为导入导出符号的手段,可免去动态链接时导出符号的二分查找导入函数DLL绑定,可免去动态链接时的符号解析,但DLL更新后或重定基地址后可能导致绑定失败。C++编写动态库注意事项:所有接口函数应该都为抽象的全局函数使用extern "C"来防止名字修饰,导出函数使用__stdcall调用规范不使用STL库、异常、虚析构函数不在DLL中申请内存而在DLL外释放内存或相反的方式,因为不同的DLL和可执行程序使用不同的堆接口不使用重载方法COM的主要目的之一是解决C++动态库的ABI兼容性问题不到万不得已,不要将大尺寸的对象或结构变量直接通过函数返回值返回,对象或结构变量的返回在实际实现上是通过两次内存拷贝完成的,效率不高线程的私有空间:栈、寄存器、TLS(线程局部存储,一般在进程堆中分配)静态链接C语言标准库crt时,如果通过ExitThread退出线程,线程私有数据_tidata将不会被释放,从而导致内存泄漏,因此最后使用经过包装后的线程函数,如C语言库的__beginthread,_endthreadLinux的每个进程都有自己的内核栈和用户栈回应 2011-12-08 13:25
-
假设我们有一个全局变量叫做var,它在目标文件A里面。我们在目标文件B里面要访问这个全局变量,比如我们在目标文件B里面有这么一条指令: movl $0x2a, var 这条指令就是给这个var变量赋值0x2a,相当于C语言里面的语句var=42。然后我们编译目标文件B,得到这条指令机器码,如图2-9所示。 (图片不方便,如后面:C7 05(mov指令码) 00 00 00 00(目标地址) 2a 00 00 00(源常量)) 由于在编译目标文件B的时候,编译器并不知道...
2018-07-17 15:08
假设我们有一个全局变量叫做var,它在目标文件A里面。我们在目标文件B里面要访问这个全局变量,比如我们在目标文件B里面有这么一条指令:
movl $0x2a, var
这条指令就是给这个var变量赋值0x2a,相当于C语言里面的语句var=42。然后我们编译目标文件B,得到这条指令机器码,如图2-9所示。
(图片不方便,如后面:C7 05(mov指令码) 00 00 00 00(目标地址) 2a 00 00 00(源常量))
由于在编译目标文件B的时候,编译器并不知道变量var的目标地址,所以编译器在没法确定地址的情况下,将这条mov指令的目标地址置为0,等待连接器在将目标文件A和B链接起来的时候再将其修正。我们假设A和B链接后,变量var的地址确定下来为0x1000,那么连接器将会把这个指令的目标地址部分改成0x10000。这个地址修正的过程也被叫做重定位(Relocation),每个要被修正的地方叫一个重定位入口(Relocation Entry)。重定位所做的就是给程序中每个这样的绝对地址引用的位置“打补丁”,使它们指向正确的地址。
通过一个小例子来形象化的说明重定位的概念。
回应 2018-07-17 15:08 -
当某个软件层面要发生变化,却要保持与之相关联的另一方面不变时,加一个中间层即可。Windows API层就是这样的一个“银弹”。 在编译器设计中,IR中间语言也是利用了上述思想。 还有这两天听吴军的“谷歌方法论”,介绍了谷歌翻译的实现过程。当语言数目大起来之后,工作量很大。比如一百种语言相互之间两两的互译大约有一万种语言对(O(n^2))。奥科想了一个便捷的方法——采用几种中间语言,将所有的语言都翻译成这几种中间语...
2018-04-22 11:03
当某个软件层面要发生变化,却要保持与之相关联的另一方面不变时,加一个中间层即可。Windows API层就是这样的一个“银弹”。
在编译器设计中,IR中间语言也是利用了上述思想。
还有这两天听吴军的“谷歌方法论”,介绍了谷歌翻译的实现过程。当语言数目大起来之后,工作量很大。比如一百种语言相互之间两两的互译大约有一万种语言对(O(n^2))。奥科想了一个便捷的方法——采用几种中间语言,将所有的语言都翻译成这几种中间语言,再从中间语言翻译成各种目标语言(O(n))。极大的减轻了工作量。大牛们都是能够灵活使用各种技巧方法的人。
知识学了,要会使用。会使用了,才说明这个知识算是基本掌握了。
回应 2018-04-22 11:03 -
除了全局对象构造和析构之外,.init和.finit还有其他的作用。由于他们的特殊性(在main之前/后执行),一些用户监控程序性能、调试等工具经常利用它们进行一些初始化和反初始化的工作。当然我们也可以使用“__attribute__(section("init"))”将函数放到.init段里面,但是要注意的是普通函数放在“.init”是会破坏它们的结构的,因为函数的返回指令使得_init()函数会提前返回,必须使用汇编指令,不能让编译器产生“ret”指令。
2018-04-22 10:44
-
论坛 · · · · · ·
小错误太多了,有没有官方的勘误表? | 来自knightley | 2018-03-11 | |
【整理一下】书中的笔误 | 来自helinbo | 10 回应 | 2018-02-23 |
询问 | 来自荣 | 1 回应 | 2017-08-27 |
书中源代码托管于 github.com/miaoski/xiuyang | 来自御宅暴君 | 2015-01-23 | |
这本书的名字让我想到周星驰电影里那本“演员的自... | 来自无瑕 | 1 回应 | 2014-01-21 |
> 浏览更多话题
0 有用 georgexsh 2012-07-30
书名太233
0 有用 麥喬 2010-12-30
代码视角的计算机
1 有用 飘飘白云 2009-10-30
代码是编译之后是如何链接,装载以及运行的,程序员的高级读本
0 有用 高端人口颈椎君 2011-10-29
学os的时候顺便买了本, 非常好的书, 讲的很到位,没有累赘
1 有用 nevercry 2012-04-17
其实 我是一名⋯⋯⋯⋯⋯⋯⋯⋯ 程序员。
0 有用 散关清渭 2019-02-01
大学的时候读了一些 感觉不是很理解 于是拿出来重读了一下 对于编译系统有了些理解 不过说来惭愧 要是大学的时候能静下心深读一下 或许很多弯路能避免吧
0 有用 蜗牛 2019-01-21
前十章写的还可以,后面的写的啰嗦,没有逻辑。
0 有用 俊东 2019-01-15
以小见大
0 有用 Downtown坏人 2019-01-06
静态链接部分写的很清晰,后半部分动态链接就有点水了
0 有用 csukuangfj 2018-12-27
不推荐.