├── README.md ├── align.md ├── array.md ├── bss.md ├── byval.md ├── call.md ├── dynamicstack.md ├── frame.md ├── gcc.md ├── globalvar.md ├── inlineasm.md ├── localvar.md ├── macro.md ├── main.md ├── mem.md ├── name.md ├── optimize.md ├── pfunc.md ├── process0.01.md ├── process2.6.md ├── recur.md ├── static.md ├── staticstack.md ├── string.md ├── struct.md └── varargs.md /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 |

朴素linux

4 | 5 |   大学里我坚持的最久的一项任务就是自学 linux 内核, 6 | 虽然以后可能也没机会从事 linux 内核方面的工作, 7 | 但是至少提升了自己的编程水平。 8 | 9 |   linux 最新内核的源代码已经没有进行全面研究的可能了, 10 | 我看的是 linux0.01 的内核源码。 11 | 没有指导直接看源代码是不太容易看懂的, 12 | 因为其中涉及到不少硬件操作的规范, 13 | 《linux内核完全注释2.01》——赵炯 著 14 | 对早期linux内核的分析是最详细的,真的达到了完全注释的地步, 15 | 虽然书里分析的是 linux0.11 的源代码, 16 | 但相对于 0.01 的改动不多。 17 | 18 |   而最新的内核由于巨大的代码量, 19 | 要达到源代码的完全注释应该是不可能的了, 20 | 但是从大粒度上进行的分析也是很有价值的。 21 | 《Linux内核设计与实现》英文名为 22 | *Linux kernerl Development* , 23 | 是 *Robert Love* 所著,陈莉君、康华、张波 翻译的, 24 | 我从这本书中了解了最新内核的进程调度思想。 25 | 26 |   还有一本书,书名是《LINUX内核源代码情景分析》 27 | 毛德操 胡希明 著,这本书对于 PCI 28 | 总线操作规范的介绍可谓完全注释。为什么我会看 PCI 29 | 总线的操作?因为现在的电脑都是用 PCI 而非早期的 ISA 30 | 总线了,linux0.01 对硬盘的操作使用 ISA 规定的固定端口, 31 | 而 PCI 总线中的硬盘的端口是动态设定的, 32 | 与 ISA 时的端口不一致了,所以如果想用 linux0.01 33 | 读写我本机的硬盘的话就得加入 PCI 的功能, 34 | 所以我才看关于最新内核的书,我是被逼的。 35 | 36 |   就快要毕业去工作了,想着写几篇文章同大家分享一下 37 | linux 和 C 语言方面的底层知识。那些既想了解底层 38 | 又不愿意系统地看源代码或操作系统方面书籍的同学可以来看看, 39 | 就当是看一部小说吧。 40 | 这个系列的名字叫朴素linux。 41 | 42 |   以下是目录,目录随着进度变更, 43 | 还有可能被重新分类整理,请见谅。 44 | 45 | 46 | 47 | * 解剖C语言 48 | 1. [照妖镜和火眼金睛](https://github.com/1184893257/simplelinux/blob/master/gcc.md#top) \[2012/11/8更新](3) 49 | 怎么获得C语言翻译后的汇编代码,怎么获得消除宏的C源程序 50 | 2. [局部变量](https://github.com/1184893257/simplelinux/blob/master/localvar.md#top) \[2012/11/12更新](4) 51 | i=3; (++i)+(++i)+(++i) 不同编译器结果不同,怎么看它们的运算过程。 52 | 3. [全局变量](https://github.com/1184893257/simplelinux/blob/master/globalvar.md#top) \[2012/11/8上线](5) 53 | 全局变量与局部变量在访问方式上有什么不同 54 | 4. [函数调用](https://github.com/1184893257/simplelinux/blob/master/call.md#top) \[2012/11/9上线](6) 55 | 调用一个函数的前前后后 56 | 5. [值传递](https://github.com/1184893257/simplelinux/blob/master/byval.md#top) \[2012/11/11上线](7) 57 | C语言只有值传递,怎么修改外部变量 58 | 6. [数组与指针](https://github.com/1184893257/simplelinux/blob/master/array.md#top) \[2012/12/23更新](8) 59 | 数组的起始地址存在哪儿? 60 | 7. [字符串](https://github.com/1184893257/simplelinux/blob/master/string.md#top) \[2012/11/15上线](9) 61 | 为什么有的字符串不能修改 62 | 8. [结构体](https://github.com/1184893257/simplelinux/blob/master/struct.md#top) \[2012/11/17上线](10) 63 | 结构体与子元素什么关系,数组不能复制? 64 | 9. [奇怪的宏](https://github.com/1184893257/simplelinux/blob/master/macro.md#top) \[2012/11/19上线](11) 65 | do{...} while(0)是何用意 66 | 10. [内存对齐](https://github.com/1184893257/simplelinux/blob/master/align.md#top) \[2012/11/28更新](12) 67 | 为什么要进行内存对齐,怎么关闭内存对齐 68 | 11. [函数帧](https://github.com/1184893257/simplelinux/blob/master/frame.md#top) \[2012/11/24上线](13) 69 | 函数的局部环境:函数帧 70 | 12. [函数帧应用一:谁调用了main?](https://github.com/1184893257/simplelinux/blob/master/main.md#top) \[2012/11/27上线](14) 71 | 不复杂 72 | 13. [函数帧应用二:所有递归都可以变循环](https://github.com/1184893257/simplelinux/blob/master/recur.md#top) \[2012/11/30上线](15) 73 | 真的可以 74 | 14. [未初始化全局变量](https://github.com/1184893257/simplelinux/blob/master/bss.md#top) \[2012/12/3上线](16) 75 | 未初始化全局变量 不跟 初始化全局变量 存一块儿 76 | 15. [进程内存分布](https://github.com/1184893257/simplelinux/blob/master/mem.md#top) \[2012/12/6上线](17) 77 | 全局变量、堆、栈 在哪儿?访问它们的特点 78 | 16. [编译优化](https://github.com/1184893257/simplelinux/blob/master/optimize.md#top) \[2012/12/9上线](18) 79 | C语言比汇编慢,怎么优化编译过程 80 | 17. [static变量 及 作用域控制](https://github.com/1184893257/simplelinux/blob/master/static.md#top) \[2012/12/12上线](19) 81 | 压缩变量的作用域,提高源代码的可读性 82 | 18. [变量名、函数名](https://github.com/1184893257/simplelinux/blob/master/name.md#top) \[2012/12/15上线](20) 83 | 变量名、函数名在哪里终结,有什么用? 84 | 19. [函数指针](https://github.com/1184893257/simplelinux/blob/master/pfunc.md#top) \[2012/12/18上线](21) 85 | 函数指针跟普通指针有什么区别 86 | 20. [可变参数](https://github.com/1184893257/simplelinux/blob/master/varargs.md#top) \[2012/12/21上线](22) 87 | 可变参数怎么实现的?变参函数的可行性? 88 | 21. [C语言的栈是静态的](https://github.com/1184893257/simplelinux/blob/master/staticstack.md#top) \[2012/12/23上线](23) 89 | 变参函数力不能及的地方 90 | 22. [内联汇编](https://github.com/1184893257/simplelinux/blob/master/inlineasm.md#top) \[2012/12/24上线](24) 91 | gcc 以及 VC 的内联汇编 92 | 23. [汇编实现的动态栈](https://github.com/1184893257/simplelinux/blob/master/dynamicstack.md#top) \[2012/12/25上线](25) 93 | 实现一个运行时的接受可变参数的printf 94 | * 内核小知识 95 | 1. [linux0.01进程时间片的消耗和再生](https://github.com/1184893257/simplelinux/blob/master/process0.01.md#top) \[2012/11/7更新](1) 96 | 2. [linux2.6.XX进程切换和时间片再生](https://github.com/1184893257/simplelinux/blob/master/process2.6.md#top) \[2012/11/7上线](2) 97 | -------------------------------------------------------------------------------- /align.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

内存对齐 8 |

9 | 10 | ## 为什么要进行内存对齐 11 | 12 |   在计算机组成原理中我们学到: 13 | 一块内存芯片一般只提供 8 位数据线,要进行 16 位数据的读写 14 | 可采用奇偶分体来组织管理多个芯片, 15 | 32 位也类似: 16 | 17 | ![align](http://fmn.rrimg.com/fmn056/20121121/1905/original_zCiC_1caa000049ee125c.jpg) 18 | 19 |   这样,连续的四个字节会分布在不同的芯片上, 20 | 送入地址 0,我们可将第 0、1、2、3 四个字节一次性读出组成 21 | 一个 32 位数,送入地址 4(每个芯片接收到的地址是1), 22 | 可一次性读出 4、5、6、7 四个字节。 23 | 24 |   但是如果要读 1、2、3、4 四个字节,就麻烦了, 25 | 有的 CPU 直接歇菜了:我处理不了! 26 | 但 Intel 的 CPU 走的是复杂指令集路线, 27 | 岂能就此认输,它通过两次内存读, 28 | 然后进行拼接合成我们想要的那个 32 位数, 29 | 而这一切是在比机器码更低级的微指令执行阶段完成的, 30 | 所以 movl 1, %eax 会不出意外地读出 1、2、3、4 四个字节 31 | 到 eax,证据如下(mem.c): 32 | 33 | #include 34 | 35 | char a[]={0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; 36 | 37 | int main() 38 | { 39 | int *p = (int*)(a + 1); 40 | int ans = *p; 41 | 42 | printf("*p:\t%p\n", ans); 43 | printf("a:\t%p\n", a); 44 | printf("p:\t%p\n", p); 45 | return 0; 46 | } 47 | 48 | `  `该程序的运行结果如下: 49 | 50 | [lqy@localhost temp]$ gcc -o mem mem.c 51 | [lqy@localhost temp]$ ./mem 52 | *p: 0x55443322 53 | a: 0x80496a8 54 | p: 0x80496a9 55 | [lqy@localhost temp]$ 56 | 57 | `  `可看出程序确实从一个未对齐到 4 字节的地址(0x80496a9) 58 | 后读出了 4 个字节,从汇编可看出确实是 1 条 mov 指令读出来的: 59 | 60 | movl $a, %eax 61 | addl $1, %eax 62 | movl %eax, 28(%esp) # 初始化指针 p 63 | movl 28(%esp), %eax 64 | movl (%eax), %eax # 这里读出了 0x55443322 65 | movl %eax, 24(%esp) # 初始化 ans 66 | 67 | `  `虽然 Intel 的 CPU 能这样处理,但还是要浪费点时间不是, 68 | 所以 C 程序还是要采取措施避免这种情况的发生, 69 | 那就是内存对齐。 70 | 71 | ## 内存对齐的结果 72 | 73 |   内存对齐的完整描述你还是去百度吧, 74 | 这里我只是含糊地介绍一下: 75 | 76 | 1. 保证最大类型对齐到它的 size 77 | 2. 尽量不浪费空间 78 | 79 | 比如: 80 | 81 | struct A{ 82 | char a; 83 | int c; 84 | }; 85 | 86 | 它的大小为 8,c 的内部偏移为 4, 87 | 这样就可以一次性读出 c 了。 88 | 89 | 再如: 90 | 91 | struct B{ 92 | char a; 93 | char b; 94 | int c; 95 | }; 96 | 97 | 它的大小还是 8,第 2 条起作用了! 98 | 99 | ## 关闭内存对齐 100 | 101 |   讲到内存对齐,估计大家最期待的一大快事就是怎么关闭它 102 | (默认是开启的),毕竟 Intel CPU 如此强大, 103 | 关闭了也没事。 104 | 105 |   关闭它也甚是简单,添加预处理指令 #pragma pack(1) 106 | 就行,windows linux 都管用: 107 | 108 | #include 109 | 110 | #pragma pack(1) 111 | 112 | struct _A{ 113 | char c; 114 | int i; 115 | }; 116 | //__attribute__((packed)); 117 | 118 | typedef struct _A A; 119 | 120 | int main() 121 | { 122 | printf("%d\n", sizeof(A)); 123 | return 0; 124 | } 125 | 126 | `  `linux gcc 中更常见的是使用 `__attribute__((packed))`, 127 | 这个属性只解除对一个结构体的内存对齐,而 #pragma pack(1) 128 | 解除了整个 C源文件 的内存对齐, 129 | 所以有时候 `__attribute__((packed))` 显得更为合理。 130 | 131 |   什么时候可能需要注意或者关闭内存对齐呢? 132 | 我想大概是这两种情况: 133 | 134 | * 结构化文件的读写 135 | * 网络数据传输 136 | 137 | ## 另一个浪费内存的家伙 138 | 139 |   说到内存对齐,我想起了另一个喜欢浪费内存的家伙: 140 | 参数对齐(我瞎编的名字,C 标准中或许有明确规定)。 141 | 看下面这个程序: 142 | 143 | #include 144 | 145 | typedef unsigned char u_char; 146 | 147 | u_char add(u_char a, u_char b) 148 | { 149 | return (u_char)(a+b); 150 | } 151 | 152 | int main() 153 | { 154 | u_char a=1, b=2; 155 | 156 | printf("ans:%d\n", add(a, b)); 157 | return 0; 158 | } 159 | 160 | `  `你说 add 函数的参数会占几个字节呢?2个?4个? 161 | 结果是 8 个…… 162 | 163 |   “可恨”的是,这个家伙浪费内存的行为却被所有编译器纵容, 164 | 我们无法追究它的责任。 165 | (应该是为了方便计算参数位置而规定的) 166 | 167 | [回目录][content] 168 | -------------------------------------------------------------------------------- /array.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1184893257/simplelinux/eb833374548a2e19140c20c679ac655bb1c29300/array.md -------------------------------------------------------------------------------- /bss.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

未初始化全局变量 8 |

9 | 10 |   为下一篇介绍进程内存分布做准备, 11 | 这一篇先来介绍一下未初始化全局变量: 12 | 13 |   未初始化全局变量,这名字就很直白,就是 C 程序中定义成 14 | 全局作用域而又没有初始化的变量,我们知道这种变量在程序运行 15 | 后是被自动初始化为 全0 的。编译器编译的时候会将这类变量 16 | 收集起来集中放置到 .bss 段中,这个段只记录了段长, 17 | 没有实际上的内容(全是0,没必要存储), 18 | 在程序被装载时操作系统会 19 | 为它分配等于段长的内存,并全部初始化为0。 20 | 21 |   这有两个 C程序,都定义了全局数组 data(长度为1M, 22 | 占用内存4MB),一个部分初始化(bss\_init1.c), 23 | 一个未初始化(bss\_uninit1.c): 24 | 25 | bss_init1.c: 26 | 27 | #include 28 | #include 29 | 30 | #define MAXLEN 1024*1024 31 | 32 | int data[MAXLEN]={1,}; 33 | 34 | int main() 35 | { 36 | Sleep(-1); 37 | return 0; 38 | } 39 | 40 | bss_uninit1.c: 41 | 42 | #include 43 | #include 44 | 45 | #define MAXLEN 1024*1024 46 | 47 | int data[MAXLEN]; 48 | 49 | int main() 50 | { 51 | Sleep(-1); 52 | return 0; 53 | } 54 | 55 | `  `编译以上两个程序后: 56 | 57 | ![bss1](http://fmn.rrfmn.com/fmn059/20121203/1935/original_4q5M_35d80000b351118d.jpg) 58 | 59 |   可以看到有初始化的可执行文件的大小差不多是4MB, 60 | 而未初始化的只有47KB!这就是 .bss 段有段长, 61 | 而没有实际内容的表现。用 UltraEdit 打开 bss_init1.exe 62 | 可看到文件中大部分是全0(data数组的内容): 63 | 64 | ![bss5](http://fmn.rrimg.com/fmn065/20121203/1935/original_RbRN_5afd0000b341125d.jpg) 65 | 66 |   但是接下来运行(return 0 之前的 Sleep(-1) 保证了 67 | 程序暂时不会退出)的时候,却发现 bss_init1.exe 68 | 占用的空间明显少于 4MB,这是怎么回事呢? 69 | 70 | ![bss2](http://fmn.rrimg.com/fmn065/20121203/1935/original_ejt4_363e0000b309118d.jpg) 71 | 72 |   这就涉及程序装载的策略了。早期的操作系统(如:linux 0.01) 73 | 采用的是一次装载:将可执行文件一次性完整装入内存后再执行程序。 74 | 不管程序是 1KB 还是 60MB,都要等全部装入内存后才能执行, 75 | 这显然是不太合理的。 76 | 77 |   而现在的操作系统都是采用延迟装载: 78 | 将进程空间映射到可执行文件之后就开始执行了, 79 | 执行的时候如果发现要读/写的页不在内存中, 80 | 就根据映射关系去读取进来,然后继续执行应用程序 81 | (应该是在页保护异常的处理中实现的)。 82 | 83 |   bss_init1.exe 肯定是被映射了,而程序中又没有对 data 84 | 数组进行读/写操作,所以操作系统也就懒得去装入这片内存了。 85 | 下面修改一下这两个程序:在 Sleep(-1) 前将 data 数组 86 | 的每个元素赋值为 -1: 87 | 88 | int i; 89 | for(i=0; i 6 | 7 |

汇编实现的动态栈 8 |

9 | 10 |   这一篇就是实现 d_printf,废话不多说,直接上代码。 11 | 由于 VC 的内联汇编还是比较清晰,那就先贴 VC 版的。 12 | 13 | ## 一、d_printf VC版 14 | 15 | #include 16 | 17 | void d_printf(const char *fmt, int n, int a[]) 18 | { 19 | static int size1, size2; 20 | static const char *fmt_copy; 21 | 22 | size1 = 4*n; // 可变参数的空间大小 23 | size2 = size1 + 4; // 还有 fmt 指针4字节, 恢复 esp 时用 24 | fmt_copy = fmt; 25 | 26 | __asm{ 27 | // 保护要修改的 ecx/esi/edi 寄存器 28 | push ecx 29 | push esi 30 | push edi 31 | 32 | // 给 ecx/esi/edi 赋值 33 | mov ecx, n // movsd 的执行次数 34 | mov esi, a // a -> esi 35 | sub esp, size1 36 | mov edi, esp // esp - size1 -> edi 37 | 38 | rep movsd // n 次4字节拷贝 39 | push fmt_copy // 压栈格式串(字符串指针) 40 | call printf 41 | 42 | add esp, size2 // 恢复栈 43 | // 恢复各个寄存器 44 | pop edi 45 | pop esi 46 | pop ecx 47 | } 48 | } 49 | 50 | int main() 51 | { 52 | char fmt[1024]; // 格式串 53 | char c; // 额外读取一个字符 54 | int a[1024]; // 存读到的整数 55 | int i; 56 | 57 | while(EOF != scanf("%s%c", fmt, &c)) // 读到 EOF 就结束 58 | { 59 | if(c == '\n') // 格式串后没有数字 60 | { 61 | printf("%s\n\n", fmt); // 直接打印, 不用 d_printf 62 | continue; 63 | } 64 | 65 | // 循环读取各个整数 66 | i = 0; 67 | do 68 | { 69 | scanf("%d%c", &a[i++], &c); 70 | }while(c != '\n'); 71 | 72 | // 调用 d_printf, i 刚好是输入的整数的个数 73 | d_printf(fmt, i, a); 74 | printf("\n\n"); // 补个换行比较好看O(∩_∩)O~ 75 | } 76 | } 77 | 78 | ## 二、d_printf gcc 版(main 函数跟 VC 版的一样) 79 | 80 | #include 81 | 82 | void d_printf(const char *fmt, int n, int a[]) 83 | { 84 | int d0, d1, d2; 85 | 86 | static int size1, size2; 87 | static const char *fmt_copy; 88 | 89 | size1 = 4*n; // 可变参数的空间大小 90 | size2 = size1 + 4; // 还有 fmt 指针4字节, 恢复 esp 时用 91 | fmt_copy = fmt; 92 | 93 | asm volatile( 94 | "subl %6, %%esp\n\t" 95 | "movl %%esp, %%edi\n\t" 96 | "rep ; movsl\n\t" 97 | "pushl %3\n\t" 98 | "call printf\n\t" 99 | "addl %7, %%esp" 100 | : "=&S"(d0), "=&D"(d1), "=&c"(d2) 101 | : "m"(fmt_copy), "0"(a), "2"(n), "m"(size1), "m"(size2)); 102 | } 103 | 104 | int main() 105 | { 106 | char fmt[1024]; // 格式串 107 | char c; // 额外读取一个字符 108 | int a[1024]; // 存读到的整数 109 | int i; 110 | 111 | while(EOF != scanf("%s%c", fmt, &c)) // 读到 EOF 就结束 112 | { 113 | if(c == '\n') // 格式串后没有数字 114 | { 115 | printf("%s\n\n", fmt); // 直接打印, 不用 d_printf 116 | continue; 117 | } 118 | 119 | // 循环读取各个整数 120 | i = 0; 121 | do 122 | { 123 | scanf("%d%c", &a[i++], &c); 124 | }while(c != '\n'); 125 | 126 | // 调用 d_printf, i 刚好是输入的整数的个数 127 | d_printf(fmt, i, a); 128 | printf("\n\n"); // 补个换行比较好看O(∩_∩)O~ 129 | } 130 | } 131 | 132 | ## 三、运行效果 133 | 134 |   linux 中的运行效果如下: 135 | 136 | [lqy@localhost temp]$ ./d_printf 137 | nospaceword 138 | nospaceword 139 | 140 | %X 256 141 | 100 142 | 143 | %d+%d=%d 1 2 3 144 | 1+2=3 145 | 146 | >%3d>%03d> 3 3 147 | > 3>003> 148 | 149 | >%3d>%-3d> 3 3 150 | > 3>3 > 151 | 152 | [lqy@localhost temp]$ 153 | 154 | `  `最后输入 EOF 结束:Ctrl + D(linux)、Ctrl + Z 155 | (windows)。由于转义符是编译时处理的,printf 是不管的, 156 | 所以\n什么的在这里不管用^_^。 157 | 158 | ## 四、为什么用 static 159 | 160 |   d\_printf 中为什么将 size1、size2、fmt_copy 声明为 161 | static 变量呢? 162 | 163 | 1. 不能用寄存器。size2 是在 call printf 之后使用的, 164 | 但是后来我发现 printf 执行完后改动了好几个寄存器的值, 165 | 所以如果将 size2 保存到某个寄存器中是不行的 166 | (难怪C语言喜欢内存,寄存器太不可靠了o(╯□╰)o)。 167 | 2. 不能用局部变量。不让用寄存器就用内存呗! 168 | 但是我们要自主修改 esp, 169 | 而局部变量有可能是通过 esp+常量偏移 定位的 170 | (如果是用 ebp+常量偏移(VC一般这么用)定位的就没问题), 171 | 所以 subl %6, %%esp 之后 到 addl %7, %%esp 之前 172 | 都不能使用局部变量,否则会定位错误。 173 | 所以才使用 static 变量, 174 | 因为 static 变量是用绝对地址定位的,跟 esp 毫无关系。 175 | 176 | ## 五、使用内联汇编的建议 177 | 178 | 1. 不要用。 179 | 2. 如果非得用的话,尽量用 C 语言实现+-*/, 180 | 如 d_printf 中给 size1、size2 赋值; 181 | 内联汇编只实现不得不用的部分(内联汇编一般都短小精悍)。 182 | 183 |

《解剖C语言》就此完结。

184 | 185 | [回目录][content] 186 | -------------------------------------------------------------------------------- /frame.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

函数帧 8 |

9 | 10 |   这标题一念出来我立刻想到了一个名人:白素贞……当然, 11 | 此女与本文无关,下面进入正题: 12 | 13 |
其实程序运行就好比一帧一帧地放电影,每一帧是一次函数调用,电影放完了,我们就看到结局了。
14 | 15 |   我们用一个递归求解阶乘的程序来看看这个放映过程(fac.c): 16 | 17 | #include 18 | 19 | int fac(int n) 20 | { 21 | if(n <= 1) 22 | return 1; 23 | return n * fac(n-1); 24 | } 25 | 26 | int main() 27 | { 28 | int n = 3; 29 | int ans = fac(n); 30 | 31 | printf("%d! = %d\n", n, ans); 32 | return 0; 33 | } 34 | 35 | ## main 帧 36 | 37 |   首先 main 函数被调用(程序可不是从 main 开始执行的): 38 | 39 | 40 | 62 | 63 |
41 |
main:
 42 | 	pushl	%ebp
 43 | 	movl	%esp, %ebp
 44 | 	andl	$-16, %esp
 45 | 	subl	$32, %esp
 46 | 	movl	$3, 28(%esp)	# n = 3
 47 | 	movl	28(%esp), %eax
 48 | 	movl	%eax, (%esp)
 49 | 	call	fac
 50 | 	movl	%eax, 24(%esp)	# 返回值存入 ans
 51 | 	movl	$.LC0, %eax
 52 | 	movl	24(%esp), %edx
 53 | 	movl	%edx, 8(%esp)
 54 | 	movl	28(%esp), %edx
 55 | 	movl	%edx, 4(%esp)
 56 | 	movl	%eax, (%esp)
 57 | 	call	printf
 58 | 	movl	$0, %eax
 59 | 	leave
 60 | 	ret
 61 | 
64 | 65 | `  `main 函数创建了一帧: 66 | 67 | * 从 esp 到 ebp + 4 68 | * 上边是本次调用的返回地址、旧的 ebp 指针 69 | * 然后是 main 的局部变量 n、ans 70 | * 最下边是参数的空间,右上图显示的是 main 中调用 printf 71 | 前的栈的使用情况 72 | 73 | `  `进入 main 函数,前 4 条指令开辟了这片空间, 74 | 在退出 main 函数之前的 leave ret 回收了这片空间 75 | (C++ 在回收这片空间之前要析构此函数中的所有局部对象)。 76 | 在 main 函数执行期间 ebp 一直指向 帧顶 - 4 的位置, 77 | ebp 被称为帧指针也就是这个原因。 78 | 79 | ## 调用惯例 80 | 81 |   调用函数的时候,先传参数,然后 call, 82 | 具体这个过程怎么实现有相关规定,这样的规定被称为调用惯例, 83 | C语言中有多种调用惯例,它们的不同之处在于: 84 | 85 | 1. 参数是压栈还是存入寄存器 86 | 2. 参数压栈的次序(从右至左 | 从左至右) 87 | 3. 调用完成后是调用者还是被调用者来恢复栈 88 | 89 | `  `各种调用惯例《程序员的自我修养》——链接、装载与库 90 | 这本书中有简要介绍,我照抄后在本文后面列出。C语言默认的 91 | 调用惯例是 cdecl: 92 | 93 | 1. 参数从右至左压栈 94 | 2. 调用完成后调用者负责恢复栈 95 | 96 | `  `可以从 printf("%d! = %d\n", n, ans); 的调用过程 97 | 中看出。 98 | 99 |   虽然 VC、gcc 都默认使用 cdecl 调用惯例, 100 | 但它们的实现却各有风格: 101 | 102 | * VC 一般是从右至左 push 参数,call,add esp, XXX 103 | * 而 gcc 在给局部变量分配空间的时候也给参数分配了足够的空间, 104 | 所以只要从右至左 mov 参数, XXX(%esp),call 就可以了, 105 | 调用者根本不用去恢复栈,因为传参数的时候并没有修改栈指针 esp。 106 | 107 | ## fac 帧 108 | 109 |   说完调用惯例我们接着来看第一次调用 fac: 110 | 111 | 112 | 131 | 132 |
113 |
fac:
114 | 	pushl	%ebp
115 | 	movl	%esp, %ebp
116 | 	subl	$24, %esp
117 | 	cmpl	$1, 8(%ebp)
118 | 	jg	.L2			# n > 1 就跳到 .L2
119 | 	movl	$1, %eax
120 | 	jmp	.L3			# 无条件跳到 .L3
121 | .L2:
122 | 	movl	8(%ebp), %eax
123 | 	subl	$1, %eax
124 | 	movl	%eax, (%esp)
125 | 	call	fac		#  fac(n-1)
126 | 	imull	8(%ebp), %eax	# eax = n * eax
127 | .L3:
128 | 	leave
129 | 	ret
130 | 
133 | 134 |   fac(3) 开辟了第一个 fac 帧: 135 | 136 | * 从 esp 到 ebp + 4(fac 还能"越界"地读到参数 n) 137 | * 上边是 返回地址、旧的 ebp 指针(指向 main 帧) 138 | * fac 没有局部变量,又浪费了很多字节 139 | * 参数占了最下边的 4 字节(需要递归时使用) 140 | 141 | `  `这时还不满足递归终止条件,于是fac(3)又递归地调用了fac(2), 142 | fac(2)又递归的调用了fac(1),到这个时候栈变成了如下情况: 143 | 144 | ![total](http://fmn.rrimg.com/fmn062/20121124/1940/original_y9zg_1a3800005b5a118e.jpg) 145 | 146 |   上图的箭头的含义很明显: 147 | 从 ebp 可回溯到所有的函数帧, 148 | 这是由于每个函数开头都来两条 pushl %ebp、movl %esp, %ebp造成的。 149 | 150 |   参数总是调用者写入,被调用者来读取(被调用者修改参数毫无意义), 151 | 这是一种默契^_^。 152 | 153 | 程序继续运行: 154 | 155 | 1. fac(1) 满足了递归终止条件,fac(1) 返回 1,fac(1)#3 帧消亡 156 | 2. 继续执行 fac(2),fac(2) 返回 1\*2,fac(2)#2 帧消亡 157 | 3. 继续执行 fac(3),fac(3) 返回 2\*3,fac(1)#1 帧消亡 158 | 4. 继续执行 main,printf 结果,返回 0,main 帧消亡 159 | 5. 继续执行 ???(且听下回分解) 160 | 161 | 最终程序结束(进程僵死,一会儿后操作系统会来收尸 162 | (回收内存及其他资源))。 163 | 164 | ## 小结 165 | 166 |   函数帧保存的是函数的一个完整的局部环境, 167 | 保证了函数调用的正确返回(函数帧中有返回地址)、 168 | 返回后继续正确地执行,因此函数帧是 C语言 能调来调去的保障。 169 | 170 |

主要的调用惯例

171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 191 | 192 | 193 | 194 | 195 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 |
调用惯例出栈方参数传递名字修饰
cdecl函数调用方从右至左的顺序压参数入栈下划线+函数名
stdcall函数本身从右至左的顺序压参数入栈下划线+函数名+@+参数的字节数, 189 | 如函数 int func(int a, double b)的修饰名是 190 | _func@12
fastcall函数本身头两个 DWORD(4字节)类型或者更少字节的参数 196 | 被放入寄存器,其他剩下的参数按从右至左的顺序入栈@+函数名+@+参数的字节数
pascal函数本身从左至右的顺序入栈较为复杂,参见pascal文档
206 | 207 | [回目录][content] 208 | -------------------------------------------------------------------------------- /gcc.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1184893257/simplelinux/eb833374548a2e19140c20c679ac655bb1c29300/gcc.md -------------------------------------------------------------------------------- /globalvar.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1184893257/simplelinux/eb833374548a2e19140c20c679ac655bb1c29300/globalvar.md -------------------------------------------------------------------------------- /inlineasm.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

内联汇编 8 |

9 | 10 |   内联汇编是指在 C/C++ 代码中嵌入的汇编代码, 11 | 与全部是汇编的汇编源文件不同,它们被嵌入到 C/C++ 的大环境中。 12 | 13 | ## 一、gcc 内联汇编 14 | 15 |   gcc 内联汇编的格式如下: 16 | 17 | asm ( 汇编语句 18 | : 输出操作数 // 非必需 19 | : 输入操作数 // 非必需 20 | : 其他被污染的寄存器 // 非必需 21 | ); 22 | 23 | `  `我们通过一个简单的例子来了解一下它的格式(gcc_add.c): 24 | 25 | #include 26 | 27 | int main() 28 | { 29 | int a=1, b=2, c=0; 30 | 31 | // 蛋疼的 add 操作 32 | asm( 33 | "addl %2, %0" // 1 34 | : "=g"(c) // 2 35 | : "0"(a), "g"(b) // 3 36 | : "memory"); // 4 37 | 38 | printf("现在c是:%d\n", c); 39 | return 0; 40 | } 41 | 42 | `  `内联汇编中: 43 | 44 | 1. 第1行是汇编语句,用双引号引起来, 45 | 多条语句用 ; 或者 \n\t 来分隔。 46 | 2. 第2行是输出操作数,都是 "=?"(var) 的形式, 47 | var 可以是任意内存变量(输出结果会存到这个变量中), 48 | ? 一般是下面这些标识符 49 | (表示内联汇编中用什么来代理这个操作数): 50 | * a,b,c,d,S,D 分别代表 eax,ebx,ecx,edx,esi,edi 寄存器 51 | * r 上面的寄存器的任意一个(谁闲着就用谁) 52 | * m 内存 53 | * i 立即数(常量,只用于输入操作数) 54 | * g 寄存器、内存、立即数 都行(gcc你看着办) 55 | 56 | 在汇编中用 %序号 来代表这些输入/输出操作数, 57 | 序号从 0 开始。为了与操作数区分开来, 58 | 寄存器用两个%引出,如:%%eax 59 | 3. 第3行是输入操作数,都是 "?"(var) 的形式, 60 | ? 除了可以是上面的那些标识符,还可以是输出操作数的序号, 61 | 表示用 var 来初始化该输出操作数, 62 | 上面的程序中 %0 和 %1 就是一个东西,初始化为 1(a的值)。 63 | 4. 第4行标出那些在汇编代码中修改了的、 64 | 又没有在输入/输出列表中列出的寄存器, 65 | 这样 gcc 就不会擅自使用这些"危险的"寄存器。 66 | 还可以用 "memory" 表示在内联汇编中修改了内存, 67 | 之前缓存在寄存器中的内存变量需要重新读取。 68 | 69 | `  `上面这一段内联汇编的效果就是, 70 | 把a与b的和存入了c。当然这只是一个示例程序, 71 | 谁要真这么用就蛋疼了, 72 | 内联汇编一般在不得不用的情况下才使用。 73 | 74 | ## 二、VC 内联汇编 75 | 76 |   gcc 内联汇编被设计得很复杂,初学者看了往往头大, 77 | 而 VC 的内联汇编就简单多了: 78 | 79 | __asm{ 80 | 汇编语句 81 | } 82 | 83 | `  `一个例子程序如下(vc_add.c): 84 | 85 | #include 86 | 87 | int main() 88 | { 89 | int a=1, b=2, c=0; 90 | 91 | // 蛋疼的 add 操作 92 | __asm{ 93 | push eax // 保护 eax 94 | 95 | mov eax, a // eax = a; 96 | add eax, b // eax = eax + b; 97 | mov c, eax // c = eax; 98 | 99 | pop eax // 恢复 eax 100 | } 101 | 102 | printf("现在c是:%d\n", c); 103 | return 0; 104 | } 105 | 106 | `  `VC 的内联汇编中可以直接以变量名的形式使用局部变量, 107 | 这就方便多了。但是, 108 | VC 内联汇编中有些变量名是保留的,比如:size, 109 | 使用这些变量名就会报错(把b改成size, 110 | 上面的程序就编译不通过了)。所以,起名字一定要小心! 111 | 112 |   因为 VC 没有输入/输出操作数列表, 113 | 它也不看你的汇编代码(直接拿去用), 114 | 所以它不知道你修改了哪些寄存器, 115 | 这些要修改的寄存器可能保存着重要数据, 116 | 所以用 push/pop 来 保护/恢复 要修改的寄存器。 117 | 而 gcc 就不需要,它能从输入/输出列表中获得丰富的信息 118 | 来调剂各个寄存器的使用, 119 | 并进行优化,所以从效率上说 VC 完败! 120 | 121 | ## 三、为什么用内联汇编 122 | 123 |   用内联汇编的主要目的是为了提高效率: 124 | 假设有一个比较文本差异的程序 diff, 125 | 它花了 99% 的时间在 strcmp 这个函数上, 126 | 如果用内联汇编实现的一个高效的 strcmp 比用 C 语言实现的快 127 | 1 倍,那么专家花在这个小小函数上的心思就能够将整个程序的效率 128 | 提高差不多 1 倍,这是很值得去做的"斤斤计较"。 129 | 130 |   还有一个目的就是为了实现 C 语言无法实现的部分, 131 | 比如说 IO 操作,还有我们上一篇中提到的自主修改 esp 寄存器 132 | 也是必须用汇编才能实现的。 133 | 134 | ## 四、memcpy 135 | 136 |   学 gcc 内联汇编最好的导师莫过于 linux 内核, 137 | 有很多常用的小函数如 memcpy、strlen、strcpy、…… 138 | 其中都有短小精悍的内联汇编版本, 139 | 如在 linux 2.6.37 中的 memcpy 函数: 140 | 141 | // 位于 /arch/x86/boot/compressed/misc.c 142 | void *memcpy(void *dest, const void *src, size_t n) 143 | { 144 | int d0, d1, d2; 145 | asm volatile( 146 | "rep ; movsl\n\t" 147 | "movl %4,%%ecx\n\t" 148 | "rep ; movsb\n\t" 149 | : "=&c" (d0), "=&D" (d1), "=&S" (d2) 150 | : "0" (n >> 2), "g" (n & 3), "1" (dest), "2" (src) 151 | : "memory"); 152 | 153 | return dest; 154 | } 155 | 156 | `  `与 gcc_add.c 相比,这个函数要复杂不少: 157 | 158 | * 关键字 volatile 是告诉 gcc 不要尝试去移动、 159 | 删除这段内联汇编。 160 | * rep ; movsl 的工作流程如下: 161 | 162 | while(ecx) { 163 | movl (%esi), (%edi); 164 | esi += 4; 165 | edi += 4; 166 | ecx--; 167 | } 168 | 169 | rep ; movsb 与此类似,只是每次拷贝的不是双字(4字节), 170 | 而是字节。 171 | * "=&D" (d1) 不是想将 edi 的最终值输出到 d1 中, 172 | 而是想告诉 gcc edi的值早就改了, 173 | 不要认为它的值还是初始化时的 dest, 174 | 避免"吝啬的" gcc 把修改了的 edi 还当做 dest 来用。 175 | 而 d0、d1、d2 在开启优化后会被 gcc 无视掉 176 | (输出到它们的值没有被用过)。 177 | 178 | `  `memcpy 先复制一个一个的双字, 179 | 到最后如果还有没复制完的(少于4个字节), 180 | 再一个一个字节地复制。 181 | 我最终实现的 d_printf 就模仿了这个函数。 182 | 183 | 深入研究:
184 | gcc 内联汇编 HOWTO 文档
185 | Linux Cross Reference——各版本 linux 内核函数检索 186 | 187 | [回目录][content] 188 | -------------------------------------------------------------------------------- /localvar.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1184893257/simplelinux/eb833374548a2e19140c20c679ac655bb1c29300/localvar.md -------------------------------------------------------------------------------- /macro.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

奇怪的宏 8 |

9 | 10 |   这一篇介绍这些奇怪的宏: 11 | 12 | ## 一、do while(0) 13 | 14 |   为了交换两个整型变量的值,前面值传递中已经用 15 | 包含指针参数的 swap 函数做到了,这次用来实现(swap.c): 16 | 17 | #include 18 | 19 | #define SWAP(a,b) \ 20 | do{ \ 21 | int t = a; \ 22 | a = b; \ 23 | b = t; \ 24 | }while(0) 25 | 26 | int main() 27 | { 28 | int c=1, d=2; 29 | int t; // 测试 SWAP 与环境的兼容性 30 | 31 | SWAP(c,d); 32 | 33 | printf("c:%d d:%d\n", c, d); 34 | return 0; 35 | } 36 | 37 | `  `这个宏看起来就有点怪了:do while(0) 是写了个循环 38 | 又不让它循环,蛋疼啊!其实不然,这样写是有妙用的: 39 | 40 |   首先,SWAP 有多条语句,如果这样写: 41 | 42 | #define SWAP(a,b) \ 43 | int t = a; \ 44 | a = b; \ 45 | b = t; 46 | 47 | `  `那么用的时候就得这么用: 48 | 49 | SWAP(c,d) 50 | 51 | `  `不能加分号!不习惯吧? 52 | 53 |   其次,使用 do{...}while(0), 54 | 中间的语句用大括号括起来了,所以是另一个命名空间, 55 | 其中的新变量 t 不会发生命名冲突。 56 | 57 |   SWAP 宏要比之前那个函数的效率要高, 58 | 因为没有发生函数调用,没有参数传递, 59 | 宏会在编译前被替换,所以只是嵌入了一小段代码。 60 | 61 | ## 二、# 62 | 63 |   标题我没打错,这里要说的就是井号,#的功能是将其后面的 64 | 宏参数进行字符串化操作。比如下面代码中的宏: 65 | 66 | #define WARN_IF(EXP) \ 67 | do{ if (EXP) \ 68 | fprintf(stderr, "Warning: " #EXP "\n"); } \ 69 | while(0) 70 | 71 | `  `那么实际使用中会出现下面所示的替换过程: 72 | 73 | WARN_IF (divider == 0); 74 | 75 | 76 | `  `被替换为 77 | 78 | do { if (divider == 0) 79 | fprintf(stderr, "Warning: " "divider == 0" "\n"); 80 | } while(0); 81 | 82 | `  `需要注意的是C语言中多个双引号字符串放在一起 83 | 会自动连接起来,所以如果 divider 为 0 的话,就会打印出: 84 | 85 | Warning: divider == 0 86 | 87 | ## 三、## 88 | 89 |   # 还是比较少用的,## 却比较流行, 90 | 在 linux0.01 中就用到过。## 被称为连接符, 91 | 用来将两个 记号(编译原理中的词汇) 连接为一个 记号。 92 | 看下面的例子吧(add.c): 93 | 94 | #include 95 | 96 | #define add(Type) \ 97 | Type add##Type(Type a, Type b){ \ 98 | return a+b; \ 99 | } 100 | 101 | // 下面两条是奇迹发生的地方 102 | add(int) 103 | add(double) 104 | 105 | int main() 106 | { 107 | int a = addint(1, 2); 108 | double d = adddouble(1.5, 1.5); 109 | 110 | printf("a:%d d:%lf\n", a, d); 111 | return 0; 112 | } 113 | 114 | `  `那两行被替换后是这个样子的: 115 | 116 | int addint(int a, int b){ return a+b; } 117 | double adddouble(double a, double b){ return a+b; } 118 | 119 | 以上内容都可以使用照妖镜看到宏被替换后的情形。 120 | 121 | [回目录][content] 122 | -------------------------------------------------------------------------------- /main.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

谁调用了main? 8 |

9 | 10 |   这是函数帧的应用之一。 11 | 12 | ## 操作可行性 13 | 14 |   从上一篇中可以发现:用帧指针 ebp 可以回溯到所有的函数帧, 15 | 那么 main 函数帧之上的函数帧自然也是可以的; 16 | 而帧中 旧ebp 的上一个四字节存的是函数的返回地址, 17 | 由这个地址我们可以判断出谁调用了这个函数。 18 | 19 | ## 准备活动 20 | 21 |   下面就是这次黑客行动的主角(up.c): 22 | 23 | #include 24 | 25 | int main() 26 | { 27 | int *p; 28 | 29 | // 以下这行内联汇编将 ebp 寄存器的值存到指针 p 中 30 | __asm__("movl %%ebp, %0" 31 | :"=m"(p)); 32 | 33 | while(p != NULL){ 34 | printf("%p\n", p[1]); 35 | p = (int*)(p[0]); 36 | } 37 | 38 | return 0; 39 | } 40 | 41 | `  `首先,请允许我使用一下 gcc 内联汇编, 42 | 这里简单的解释一下: 43 | 44 | 1. "=m"(p) 表示将内存变量 p 作为一个输出操作数 45 | 2. %0 代表的是第一个操作数,那就是 p 了 46 | 3. 为了与操作数区别开来,寄存器要多加个 %, 47 | %%ebp 表示的就是 ebp 寄存器 48 | 49 | `  `总之,这块内联汇编将 ebp 寄存器的值赋给了指针 p。 50 | 51 |   然后解释一下while循环:循环中,首先打印 p[1], 52 | p[1]就是该帧所存的返回地址;然后将指针 p 改为 p[0], 53 | p[0]是 旧ebp(上一帧的帧指针); 54 | 这样,程序将按照调用顺序的逆序打印出各个返回地址。 55 | 56 |   为什么终止条件是 p==NULL 呢?这是 gcc 为了支援我们的 57 | 黑客行动特意在开始执行程序的时候将 ebp 清零了, 58 | 所以第一次执行某个函数的时候压栈的 旧ebp 是 NULL。 59 | 60 | ## 开始行动 61 | 62 |   我们使用静态链接的方式编译 up.c 63 | (静态链接的可执行文件中包含所有用户态下执行的代码), 64 | 然后执行它: 65 | 66 | [lqy@localhost temp]$ gcc -static -o up up.c 67 | [lqy@localhost temp]$ ./up 68 | 0x8048464 69 | 0x80481e1 70 | [lqy@localhost temp]$ 71 | 72 | ## 分析结果 73 | 74 |   up 打印了了两个指向代码区的地址, 75 | 接着就看它们是属于哪两个函数了: 76 | 77 | nm up | sort > up.txt 78 | 79 | * nm up 可列出各个全局函数的地址 80 | * | sort > up.txt 通过管道将 nm up 的输出作为 sort 的输入, 81 | sort 排序后输出重定向到 up.txt 文件中(输出有1910行, 82 | 不得不这么做o(╯□╰)o) 83 | 84 | `  `然后发现两个地址分别位于 `__libc_start_main`、_start 中: 85 | 86 | ... 87 | 08048140 T _init 88 | 080481c0 T _start 89 | 080481f0 t __do_global_dtors_aux 90 | 08048260 t frame_dummy 91 | 080482bc T main 92 | 08048300 T __libc_start_main 93 | 080484d0 T __libc_check_standard_fds 94 | ... 95 | 96 | `  `实际上程序正好是从 _start 开始执行的, 97 | 而且从 up 的反汇编结果中可看出 _start 的第一条指令 98 | xor %ebp,%ebp 就是那条传说中的将 ebp 清零的指令 99 | (两个一样的数相异或的结果一定是0)。 100 | 101 |   那么调用 main 函数之前程序都干了些啥事呢? 102 | 比如说堆的初始化,如果是 C++ 程序的话, 103 | 全局对象的构造也是在 main 之前完成的 104 | (不能让 main 中使用全局对象的时候竟然还没构造吧!), 105 | 而全局对象的析构也相当有趣地在 main 执行完了之后才执行。 106 | 107 |   main 在你心目中的地位是不是一落千丈了? 108 | 109 | [回目录][content] 110 | -------------------------------------------------------------------------------- /mem.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

进程内存分布 8 |

9 | 10 |   之前一直在分析栈,栈这个东西的作用也介绍得差不多了, 11 | 但是栈在哪儿还没有搞清楚,以及堆、代码、全局变量它们在哪儿, 12 | 这都牵涉到进程的内存分布。 13 | 14 | ## linux 0.01 的进程内存分布 15 | 16 |   内存分布随着操作系统的更新换代,越来越科学合理, 17 | 也越来越复杂,所以我们还是先了解一下早期操作系统的典型 18 | linux 0.01 的进程的内存分布: 19 | 20 |   linux 0.01 的一个进程固定拥有64MB的线性内存空间 21 | (ACM竞赛中单个程序的最大内存占用限制为64MB, 22 | 这肯定有猫腻O(∩_∩)O~),各个进程挨个放置在一张页目录表中, 23 | 一个页目录表可管理4G的线性空间,因此 linux0.01 最多有 24 | 64个进程。每个进程的内存分布如下: 25 | 26 | ![mem1](http://fmn.rrimg.com/fmn061/20121206/1925/original_tXyg_61c80000059b118d.jpg) 27 | 28 | * .text 里存的是机器码序列 29 | * .rodata 里存的是源字符串等只读内容 30 | * .data 里存的是初始化的全局变量 31 | * .bss 上一篇介绍过了,存的是未初始化的全局变量 32 | * 堆、栈就不用介绍了吧! 33 | 34 | `  `.text .rodata .data .bss 是常驻内存的, 35 | 也就是说进程从开始运行到进程僵死它们一直蹲在那里, 36 | 所以访问它们用的是常量地址;而栈是不断的加帧(函数调用) 37 | 、减帧(函数返回)的,帧内的局部变量只能用相对于当前 38 | esp(指向栈顶)或 ebp(指向当前帧)的相对地址来访问。 39 | 40 |   栈被放置在高地址也是有原因的: 41 | 调用函数(加帧)是减 esp 的,函数返回(减帧)是加 esp 的, 42 | 调用在前,所以栈是向低地址扩展的,放在高地址再合适不过了。 43 | 44 | ## 现代操作系统的进程内存分布 45 | 46 |   认识了 linux 0.01 的内存分布后, 47 | 再看看现代操作系统的内存分布发生了什么变化: 48 | 49 |   首先,linux 0.01 进程的64MB内存限制太过时了, 50 | 现在的程序都有潜力使用到 2GB、3GB 的内存空间 51 | (每个进程一张页目录表),当然,机器有硬伤的话也没办法, 52 | 我的电脑就只有 2GB 的内存,想用 3GB 的内存是没指望了。 53 | 但也不是有4GB内存就可以用4GB(32位), 54 | 因为操作系统还要占个坑呢! 55 | 现代 linux 中 0xC0000000 以上的 1GB 空间是操作系统专用的, 56 | 而 linux 0.01 中第1个 64MB 是操作系统的坑, 57 | 所以别的进程完全占有它们的 64MB, 58 | 也不用跟操作系统客气。 59 | 60 |   其次,linux 0.01只有进程没有线程, 61 | 但是现代 linux 有多线程了 62 | (linux 的线程其实是个轻量级的进程), 63 | 一个进程的多个线程之间共享全局变量、堆、打开的文件…… 64 | 但栈是不能共享的:栈中各层函数帧代表着一条执行线索, 65 | 一个线程是一条执行线索,所以每个线程独占一个栈, 66 | 而这些栈又都必须在所属进程的内存空间中。 67 | 68 |   根据以上两点,进程的内存分布就变成了下面这个样子: 69 | 70 | ![mem2](http://fmn.xnpic.com/fmn056/20121206/1925/original_22bc_2556000005a8118c.jpg) 71 | 72 |   再者,如果把动态装载的动态链接库也考虑进去的话, 73 | 上面的分布图将会更加"破碎"。 74 | 75 |   如果我们的程序没有采用多线程的话, 76 | 一般可以简单地认为它的内存分布模型是 linux 0.01 的那种。 77 | 78 | [回目录][content] 79 | -------------------------------------------------------------------------------- /name.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

变量名、函数名 8 |

9 | 10 |   C程序在执行的时候直接用内存地址去定位变量、函数, 11 | 而不是根据名字去搜索,所以C程序执行的速度比脚本语言要快不少。 12 | 13 |   对于函数中的局部变量来说,编译为汇编的时候, 14 | 名字就已经被彻彻底底地忘记了, 15 | 因为局部变量在函数帧中,这一帧要占多少字节, 16 | 各局部变量在帧中的相对位置, 17 | 都在编译成汇编的时候就可以确定下来, 18 | 生成目标文件、可执行文件的时候也不需要再更改。 19 | 20 |   而 全局变量、static变量、函数 由于要将所有目标文件、 21 | 库链接到一起之后才能最终确定它们的绝对地址, 22 | 所以在链接前名字还是标志着它们的存在。 23 | 它们的信息存储在符号表(符号数组)中, 24 | 其中每一项除了有符号名,还有符号地址(链接后填入), 25 | 所以 nm 命令可得到 地址-符号名 映射。 26 | 虽然程序运行时用不到符号表, 27 | 但是默认情况下可执行文件中还是存着符号表, 28 | 看下面这个程序(name.c): 29 | 30 | #include 31 | 32 | int globalvar; 33 | 34 | int main() 35 | { 36 | static int staticval; 37 | return 0; 38 | } 39 | 40 | `  `name.c 中有全局变量、static变量、函数(main), 41 | 查看它编译后的目标文件、可执行文件的 地址-符号 映射: 42 | 43 | [lqy@localhost notlong]$ gcc -c name.c 44 | [lqy@localhost notlong]$ nm name.o 45 | 00000004 C globalvar 46 | 00000000 T main 47 | 00000000 b staticval.1672 48 | [lqy@localhost notlong]$ gcc -o name name.c 49 | [lqy@localhost notlong]$ nm name | sort 50 | 08048274 T _init 51 | 080482e0 T _start 52 | 08048310 t __do_global_dtors_aux 53 | 08048370 t frame_dummy 54 | 08048394 T main 55 | ... 56 | 此处省略X行 57 | ... 58 | 08049604 b staticval.1672 59 | 08049608 B globalvar 60 | 0804960c A _end 61 | U __libc_start_main@@GLIBC_2.0 62 | w __gmon_start__ 63 | w _Jv_RegisterClasses 64 | [lqy@localhost notlong]$ 65 | 66 | `  `可执行文件中的 地址-符号 映射还有什么存在的意义呢? 67 | 它可用于汇编级调试的时候设置断点, 68 | 比如linux内核编译后就生成了 System.map 文件, 69 | 便于进行内核调试: 70 | 71 | 00000000 A VDSO32_PRELINK 72 | 00000040 A VDSO32_vsyscall_eh_frame_size 73 | 000001d3 A kexec_control_code_size 74 | 00000400 A VDSO32_sigreturn 75 | 0000040c A VDSO32_rt_sigreturn 76 | 00000414 A VDSO32_vsyscall 77 | 00000424 A VDSO32_SYSENTER_RETURN 78 | 01000000 A phys_startup_32 79 | c1000000 T _text 80 | c1000000 T startup_32 81 | c1000054 t default_entry 82 | c1001000 T wakeup_pmode_return 83 | c100104c t bogus_magic 84 | c100104e t save_registers 85 | c100109d t restore_registers 86 | c10010c0 T do_suspend_lowlevel 87 | c10010d6 t ret_point 88 | c10010e8 T _stext 89 | c10010e8 t cpumask_weight 90 | c10010f9 t run_init_process 91 | c1001112 t init_post 92 | c10011b0 T do_one_initcall 93 | ... 94 | 95 | [回目录][content] 96 | -------------------------------------------------------------------------------- /optimize.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

编译优化 8 |

9 | 10 |   C语言没有汇编快,因为C语言要由编译器翻译为汇编, 11 | 编译器毕竟是人造的,翻译出来的汇编源代码总有那么N条 12 | 指令在更智能、更有创造性的我们看来是多余的。 13 | 14 |   C语言翻译后的汇编有如下恶劣行径: 15 | 16 | 1. C语言偏爱内存。我们写的汇编一般偏爱寄存器, 17 | 寄存器比内存要快很多倍。当然,寄存器的数量屈指可数, 18 | 数据多了的话也必须用内存。 19 | 2. 内存多余读。假如在一个 for 循环中经常要执行 20 | ++i 操作,编译后的汇编可能是这样的情形: 21 | 22 | movl i, %eax 23 | addl $1, %eax 24 | movl %eax, i 25 | 26 | 即使 eax 寄存器一直存着 i 的值, 27 | C语言也喜欢操作它前先读一下,以上3条指令浓缩为一条 28 | incl %eax 速度就快上好几倍了。 29 | 30 | `  `尽管C语言"如此不堪",但是考虑到高级语言带来的 31 | 源码可读性和开发效率在数量级上的提高,我们还是原谅了它。 32 | 而且很多编译器都有提供优化的选项, 33 | 开启优化选项后C语言翻译出来的汇编代码几近无可挑剔。 34 | 35 |   VC、VS有 Debug、Release 编译模式, 36 | Release 下编译后,程序的大小、执行效率都有显著的改善。 37 | gcc 也有优化选项,我们来看看 gcc 优化的神奇效果: 38 | 39 |   我故意写了一个垃圾程序(math.c): 40 | 41 | #include 42 | 43 | int main() 44 | { 45 | int a=1, b=2; 46 | int c; 47 | 48 | c = a + a*b + b; 49 | 50 | printf("%d\n", c); 51 | return 0; 52 | } 53 | 54 | 且看看不优化的情况下,汇编代码有多么糟糕: 55 | 56 | 编译命令:gcc -S math.c 57 | main部分的汇编代码: 58 | 59 | main: 60 | pushl %ebp 61 | movl %esp, %ebp 62 | andl $-16, %esp 63 | subl $32, %esp 64 | movl $1, 28(%esp) # 28(%esp) 是 a 65 | movl $2, 24(%esp) # 24(%esp) 是 b 66 | movl 24(%esp), %eax #\ 67 | addl $1, %eax #-\ 68 | imull 28(%esp), %eax #-eax=(b+1)*a 69 | addl 24(%esp), %eax #\ 70 | movl %eax, 20(%esp) #-c=(b+1)*a+b 71 | movl $.LC0, %eax 72 | movl 20(%esp), %edx 73 | movl %edx, 4(%esp) 74 | movl %eax, (%esp) 75 | call printf 76 | movl $0, %eax 77 | leave 78 | ret 79 | 80 | 汇编代码规模庞大,翻译水平中规中矩。 81 | 现在开启优化选项: 82 | 83 | 编译命令:gcc -O2 -S math.c 84 | 85 | main: 86 | pushl %ebp 87 | movl %esp, %ebp 88 | andl $-16, %esp 89 | subl $16, %esp 90 | movl $5, 4(%esp) 91 | movl $.LC0, (%esp) 92 | call printf 93 | xorl %eax, %eax 94 | leave 95 | ret 96 | 97 | `  `规模变为原来的一半,而且 gcc 发现了 a、b、c 98 | 变量是多余的,直接将结果 5 传给 printf 打印了出来 99 | ——计算器是编译器必备的一大技能。 100 | 初中那时候苦逼地做计算题,怎么就不学学C语言呢O(∩_∩)O~ 101 | 102 | [回目录][content] 103 | -------------------------------------------------------------------------------- /pfunc.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

函数指针 8 |

9 | 10 | ## 一、函数指针的值 11 | 12 |   函数指针跟普通指针一样,存的也是一个内存地址, 13 | 只是这个地址是一个函数的起始地址, 14 | 下面这个程序打印出一个函数指针的值(func1.c): 15 | 16 | #include 17 | 18 | typedef int (*Func)(int); 19 | 20 | int Double(int a) 21 | { 22 | return (a + a); 23 | } 24 | 25 | int main() 26 | { 27 | Func p = Double; 28 | printf("%p\n", p); 29 | return 0; 30 | } 31 | 32 | `  `编译、运行程序: 33 | 34 | [lqy@localhost notlong]$ gcc -O2 -o func1 func1.c 35 | [lqy@localhost notlong]$ ./func1 36 | 0x80483d0 37 | [lqy@localhost notlong]$ 38 | 39 | `  `然后我们用 nm 工具查看一下 Double 的地址, 40 | 看是不是正好是 0x80483d0: 41 | 42 | [lqy@localhost notlong]$ nm func1 | sort 43 | 08048294 T _init 44 | 08048310 T _start 45 | 08048340 t __do_global_dtors_aux 46 | 080483a0 t frame_dummy 47 | 080483d0 T Double 48 | 080483e0 T main 49 | ... 50 | 51 | `  `不出意料,Double 的起始地址果然是 0x080483d0。 52 | 53 | ## 二、调用函数指针指向的函数 54 | 55 |   直接调用一个函数是 call 一个常量, 56 | 而通过函数指针调用一个函数显然不能这么做, 57 | 因为函数地址是可变的了,指向谁就得 call 谁。 58 | 下面比较一下直接调用和通过函数指针间接调用同一个函数的 59 | 汇编代码(func2.c): 60 | 61 | #include 62 | 63 | typedef int (*Func)(int); 64 | 65 | int Double(int a) 66 | { 67 | return (a + a); 68 | } 69 | 70 | int main() 71 | { 72 | Func p = Double; 73 | Double(2); // 直接调用 74 | p(2); // 间接调用 75 | return 0; 76 | } 77 | 78 | `  `部分汇编代码如下: 79 | 80 | movl $2, (%esp) 81 | call Double 82 | movl $2, (%esp) 83 | movl 28(%esp), %eax # 28(%esp) 是 p 84 | call *%eax 85 | 86 | `  `可见通过函数指针间接调用一个函数, 87 | call 指令的操作数不再是一个常量, 88 | 而是寄存器 eax(其它寄存器应该也行), 89 | 此时 eax 寄存器的值正好是 Double 函数的起始地址, 90 | 所以接着就会去执行 Double 函数的指令。 91 | 92 | ## 三、参数弱匹配 93 | 94 |   从上面的例子中我们也看到了函数指针也没什么特别的, 95 | 也就存了个地址,但是调用一个函数不仅需要知道它的起始地址, 96 | 还得根据它的参数列表来压栈传递参数。 97 | 98 |   参数列表在定义函数指针类型的时候就约定好了, 99 | 凡是具有相同参数列表的函数都可以赋值给该类型的函数指针, 100 | 而参数列表不同的函数也可以通过强制类型转换后赋值给它 101 | (C语言的指针类型可以任意转换⊙﹏⊙), 102 | 下面这个程序就大胆的强制转换了一下(func3.c): 103 | 104 | #include 105 | 106 | typedef int (*Func)(int); 107 | 108 | int Double2(int a, int b) 109 | { 110 | return (a + a); 111 | } 112 | 113 | int main() 114 | { 115 | Func p = (Func)Double2; 116 | printf("%d\n", p(2)); 117 | return 0; 118 | } 119 | 120 | `  `不强制转换的话,编译的时候会报告一个 warring 121 | (居然不是 error ⊙﹏⊙), 122 | 上面这个程序编译的时候 0 error 0 warring, 123 | 执行也没有出错: 124 | 125 | [lqy@localhost notlong]$ gcc -o func3 func3.c 126 | [lqy@localhost notlong]$ ./func3 127 | 4 128 | [lqy@localhost notlong]$ 129 | 130 | `  `真算是朵奇葩了! 131 | 132 |   没有出错的原因是:参数 a 对应的刚好是压栈的 2, 133 | 而 b 对应的是一个危险地带,还好没用到 b, 134 | 所以这个程序依然顺利地执行完了。 135 | 136 |   综上所述,函数指针真没什么特别的。 137 | 138 | [回目录][content] 139 | -------------------------------------------------------------------------------- /process0.01.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1184893257/simplelinux/eb833374548a2e19140c20c679ac655bb1c29300/process0.01.md -------------------------------------------------------------------------------- /process2.6.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1184893257/simplelinux/eb833374548a2e19140c20c679ac655bb1c29300/process2.6.md -------------------------------------------------------------------------------- /recur.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

所有递归都可以变循环 8 |

9 | 10 |   这是函数帧的应用之二。 11 | 12 |   还记得大一的C程序设计课上讲到汉诺塔的时候老师说: 13 | 所有递归都可以用循环实现。这听起来好像可行, 14 | 然后我就开始想怎么用循环来解决汉诺塔问题, 15 | 我大概想了一个星期,最后终于选择了……放弃…… 16 | 当然,我不是来推翻标题的, 17 | 随着学习的深入,以及"自觉修炼",现在我可以肯定地告诉大家: 18 | 所有递归都可以用循环实现,更确切地说: 19 | 所有递归都可以用循环+栈实现 20 | (就多个数据结构,还不算违规吧O(∩_∩)O~)。 21 | 22 |   通过在我们自定义的栈中自建函数帧, 23 | 我们可以达到和函数调用一样的效果。 24 | 但是因为这样做还是比较麻烦,所以就不转换汉诺塔问题了, 25 | 而是转换之前的那个递归求解阶乘的程序(fac.c): 26 | 27 | #include 28 | 29 | int fac(int n) 30 | { 31 | if(n <= 1) 32 | return 1; 33 | return n * fac(n-1); 34 | } 35 | 36 | int main() 37 | { 38 | int n = 3; 39 | int ans = fac(n); 40 | 41 | printf("%d! = %d\n", n, ans); 42 | return 0; 43 | } 44 | 45 | ## 技术难点 46 | 47 |   我们可以在自建的函数帧中存储局部变量、存储参数, 48 | 但是我们不能存返回地址,因为我们得不到机器指令的地址! 49 | 不过,C语言有一个类似于指令地址的东西:switch case 50 | 中的 case子句,我们可以用一个case代表一个地址, 51 | 技术难点就此突破了。 52 | 53 | ## 源程序 54 | 55 |   虽然我简化了很多步骤,但源程序还是比较长(fac2.c): 56 | 57 | #include 58 | 59 | // 栈的设置 60 | #define STACKDEEPTH 1024 61 | int stack[STACKDEEPTH]; 62 | int *esp = &stack[STACKDEEPTH]; 63 | #define PUSH(a) *(--esp) = a 64 | #define POP(b) b = *(esp++) 65 | 66 | // 其它模拟寄存器 67 | int eax;// 存返回值 68 | int eip;// 用于分支选择 69 | 70 | int main() 71 | { 72 | int n = 3; 73 | 74 | // 模仿 main 调用 fac(n) 75 | PUSH(n); 76 | PUSH(10002);// 模仿返回 main 的地址 77 | eip = 10000; 78 | 79 | do{ 80 | switch(eip){ 81 | case 10000: 82 | --esp;// 为帧分配空间 83 | if(esp[2] <= 1){// 模仿递归终止条件 84 | eax = 1; 85 | ++esp;// 回收帧空间 86 | POP(eip); 87 | }else{// 模仿递归计算 fac(n-1) 88 | esp[0] = esp[2] - 1; 89 | PUSH(10001); 90 | eip = 10000; 91 | } 92 | break; 93 | 94 | case 10001:// 返回 n * (fac(n-1)的结果) 95 | eax = esp[2] * eax; 96 | ++esp;// 回收帧空间 97 | POP(eip); 98 | break; 99 | } 100 | }while(eip != 10002); 101 | 102 | printf("%d! = %d\n", n, eax); 103 | return 0; 104 | } 105 | 106 | ## 自建的函数帧 107 | 108 |   为了简化程序,ebp我们就不用了, 109 | 完全用esp来操作栈,一个函数帧只占用 8 个字节: 110 | 111 | ![recur1](http://fmn.rrimg.com/fmn063/20121130/1830/original_OJ3e_0ad000003200125b.jpg) 112 | 113 |   在计算到 fac(1) 的时候,栈中内容如下: 114 | 115 | ![recur2](http://fmn.rrimg.com/fmn056/20121130/1830/original_LBeS_30f500003160118f.jpg) 116 | 117 |   比起肆意挥霍栈空间的 gcc(fac帧用了32字节, 118 | 浪费了20字节,实际使用了12字节), 119 | 我们的程序真的是太节省了(一帧只用8字节)。 120 | 121 | ## 小结 122 | 123 |   当然,本文的方法只用于学术讨论, 124 | 说明所有递归都可以变循环,编程的时候还是不要这么用。 125 | 因为代码复杂、容易出错、难以理解, 126 | 唯一的优点是能省空间省到极限。 127 | 128 |   这种递归变循环的方式并没有降低时间复杂度, 129 | 但却是通用的(所有递归都可以这么变循环); 130 | 而有一部分递归可以基于巧妙的算法变成循环, 131 | 并且大大降低时间复杂度,如:动态规划、贪心算法 132 | (详见《算法导论》)。 133 | 134 | [回目录][content] 135 | -------------------------------------------------------------------------------- /static.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

static变量 及 作用域控制 8 |

9 | 10 | ## 一、static变量 11 | 12 |   static变量放在函数中,就只有这个函数能访问它; 13 | 放在函数外就只有这个文件能访问它。 14 | 下面我们看看两个函数中重名的static变量是怎么区别开来的 15 | (static.c): 16 | 17 | #include 18 | 19 | void func1() 20 | { 21 | static int n = 1; 22 | n++; 23 | } 24 | 25 | void func2() 26 | { 27 | static int n = 2; 28 | n++; 29 | } 30 | 31 | int main() 32 | { 33 | return 0; 34 | } 35 | 36 | `  `下面是编译后的部分汇编: 37 | 38 | func1: 39 | pushl %ebp 40 | movl %esp, %ebp 41 | movl n.1671, %eax 42 | addl $1, %eax 43 | movl %eax, n.1671 44 | popl %ebp 45 | ret 46 | 47 | func2: 48 | pushl %ebp 49 | movl %esp, %ebp 50 | movl n.1674, %eax 51 | addl $1, %eax 52 | movl %eax, n.1674 53 | popl %ebp 54 | ret 55 | 56 | `  `好家伙!编译器居然"偷偷"地改了变量名, 57 | 这样两个static变量就容易区分了。 58 | 59 |   其实static变量跟全局变量一样被放置在 .data段 或 60 | .bss段 中,所以它们也是程序运行期间一直存在的, 61 | 最终也是通过绝对地址来访问。 62 | 但是它们的作用域还是比全局变量低了一级: 63 | static变量被标识为LOCAL符号,全局变量被标识为GLOBAL符号, 64 | 在链接过程中,目标文件寻找外部变量时只在GLOBAL符号中找, 65 | 所以static变量别的源文件是"看不见"的。 66 | 67 | ## 二、作用域控制 68 | 69 |   作用域控制为的是提高源代码的可读性, 70 | 一个变量的作用域越小,它可能出没的范围就越小。 71 | 72 |   C语言中的变量按作用域从大到小可分为四种: 73 | 全局变量、函数外static变量、函数内static变量、局部变量: 74 | 75 | 1. 全局变量是杀伤半径最大的:不仅在定义该变量的源文件中可用, 76 | 而且在任一别的源文件中只要用 extern 声明它后也可以使用, 77 | 因此,当你看到一个全局变量的时候应该心生敬畏! 78 | 2. 函数外的static变量处于文件域中, 79 | 只有定义它的源文件中可以使用。如果你看到一个static变量, 80 | 那是作者在安慰你:哥们(妹子),这个变量不会在别的文件中出现。 81 | 3. 函数内static变量在函数的每次调用中可用(只初始化一次), 82 | 它同以上两种变量一样在程序运行期间一直存在, 83 | 所以它的功能是局部变量无法实现的。 84 | 4. 局部变量在函数的一次调用中使用, 85 | 调用结束后就消失了。 86 | 87 | `  `显然,作用域越小越省心, 88 | 该是局部变量的就不要定义成全局变量, 89 | 如果"全局变量"只在本源文件中使用那就加个static。 90 | 91 |   即便是局部变量也还可以压缩其作用域: 92 | 93 |   有的同学写的函数一开头就声明了函数中要用到的所有局部变量, 94 | 一开始我也这么做,因为我担心:如果把变量定义在循环体内, 95 | 是不是每一次循环都会给它们分配空间、回收空间,从而降低效率? 96 | 但事实是它们的空间在函数的开头就一次性分配好了(scope.c): 97 | 98 | #include 99 | 100 | int main() 101 | { 102 | int a = 1; 103 | { 104 | int a = 2; 105 | { 106 | int a = 3; 107 | } 108 | { 109 | int a = 4; 110 | } 111 | } 112 | return 0; 113 | } 114 | 115 | 编译后的汇编代码如下: 116 | 117 | main: 118 | pushl %ebp 119 | movl %esp, %ebp 120 | subl $16, %esp 121 | movl $1, -4(%ebp) 122 | movl $2, -8(%ebp) 123 | movl $3, -12(%ebp) 124 | movl $4, -16(%ebp) 125 | movl $0, %eax 126 | leave 127 | ret 128 | 129 | `  `各层局部环境中的变量a是subl $16, %esp一次性分配好的。 130 | 由此可见不是每个{}都要分配回收局部变量, 131 | 一个函数只分配回收一次。因此, 132 | 如果某个变量只在某个条件、循环中用到的话, 133 | 还是在条件、循环中定义吧,这样, 134 | 规模比较大的函数的可读性将提高不少,而效率丝毫没有下降, 135 | 可谓是百利而无一害! 136 | 137 | [回目录][content] 138 | -------------------------------------------------------------------------------- /staticstack.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

C语言的栈是静态的 8 |

9 | 10 |   C语言有了可变参数之后,我们可以传任意个数的参数了, 11 | 似乎挺动态的了,但是可变参数函数还是不够动态。 12 | 13 | ## 一、鞭长莫及 14 | 15 |   我们可以在 main 中写出好几条参数个数不同的调用 sum 的语句, 16 | 但是具体到某一条语句,sum 的参数个数是一定的, 17 | 比如上一篇中的 sum(2, 3, 4) 的参数个数是 3。 18 | 如果程序运行中调用 sum 函数的时候, 19 | 参数个数根据用户输入而定,那就不能用可变参数来实现了。 20 | 也就是说不能用 sum 来实现以下这个函数的功能: 21 | 22 | // 将数组 a 的所有元素(个数为 n)求和后返回 23 | int d_sum(int n, int a[]); 24 | 25 | `  `当然,这个函数不用 sum 来做是很好实现的。 26 | 我再换一个问题,下面这个函数怎么用 printf 来实现: 27 | 28 | // fmt 存的是格式串,它描述了 n 个整数(数组 a 中) 29 | // 的格式,某次调用如下: 30 | // int a[] = {1, 2, 3}; 31 | // d_printf("%d+%d=%d", 3, a); 32 | void d_printf(const char *fmt, int n, int a[]); 33 | 34 | `  `这就没法做了吧! 35 | 36 | ## 二、寻根究底 37 | 38 |   d_printf 没法实现的原因是这样的代码真没法写: 39 | 传给 printf 的参数的个数到运行的时候才知道, 40 | 而调用 printf 的语句又必须明确的列出所有参数。 41 | 42 |   其根本原因是C语言的栈是静态的, 43 | 上一篇的 va.c 编译后的汇编代码如下: 44 | 45 | main: 46 | pushl %ebp 47 | movl %esp, %ebp 48 | andl $-16, %esp 49 | subl $16, %esp # 给main帧分配栈空间 50 | movl $4, 8(%esp) 51 | movl $3, 4(%esp) 52 | movl $2, (%esp) 53 | call sum # 调用变参函数 sum 54 | movl $.LC0, (%esp) 55 | movl %eax, 4(%esp) 56 | call printf # 调用变参函数 printf 57 | xorl %eax, %eax 58 | leave 59 | ret 60 | 61 | `  `可以看到虽然 main 函数中调用了两个变参函数, 62 | 但是栈却没有一点动态可变的意思,居然是用一条 subl $16, %esp 63 | 分配了固定的 16 字节的栈空间 64 | (编译的时候计算得出需要12字节,取整吧,16字节!)。 65 | 66 |   而在 d_printf 的实现中需要分配 4+4*n 字节的栈空间, 67 | 用于存传给 printf 的 格式串指针 和 n个整数, 68 | 用C语言是没法实现啰。 69 | 70 | ## 三、另辟蹊径 71 | 72 |   C语言不能直接使用寄存器,但是汇编可以, 73 | 如果我们在 d_printf 中嵌入一段汇编来修改 esp 寄存器, 74 | 达到动态分配栈空间的效果,然后存入参数,call printf, 75 | 就可以完成任务了。 76 | 77 |   接下来的两篇就来实现 d_printf 啰! 78 | 79 | [回目录][content] 80 | -------------------------------------------------------------------------------- /string.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1184893257/simplelinux/eb833374548a2e19140c20c679ac655bb1c29300/string.md -------------------------------------------------------------------------------- /struct.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

结构体 8 |

9 | 10 |   结构体是 C 语言主要的自定义类型方案, 11 | 这篇就来认识一下结构体。 12 | 13 | ## 一、结构体的形态 14 | 15 |   C源程序(struct.c): 16 | 17 | #include 18 | 19 | typedef struct{ 20 | unsigned short int a; 21 | unsigned short int b; 22 | }Data; 23 | 24 | int main() 25 | { 26 | Data c, d; 27 | 28 | c.a = 1; 29 | c.b = 2; 30 | d = c; 31 | 32 | printf("d.a:%d\nd.b:%d\n", d.a, d.b); 33 | return 0; 34 | } 35 | 36 | `  `赋值部分翻译后: 37 | 38 | movw $1, 28(%esp) # c.a = 1 39 | movw $2, 30(%esp) # c.b = 2 40 | movl 28(%esp), %eax # 41 | movl %eax, 24(%esp) # d = c 42 | 43 | `  `可以看出: 44 | 45 | * c.a 是在 28(%esp) 之后的2个字节 46 | * c.b 是在 30(%esp) 之后的2个字节 47 | * c 是 28(%esp) 之后的4个字节 48 | * d 是 24(%esp) 之后的4个字节 49 | 50 | `  `不得不感叹名字(结构体名字、子元素名字)再一次被抛弃了, 51 | 子元素名代表的是相对于结构体的偏移。 52 | 53 | ## 二、结构体的复制 54 | 55 |   大一的时候,老师千叮咛万嘱咐:数组不能复制!, 56 | 但是当发现下面这个程序正常运行后,我困惑了(block.c): 57 | 58 | #include 59 | 60 | typedef struct{ 61 | char data[1000]; 62 | }Block; 63 | 64 | Block a={{'a','b','c',}}; 65 | 66 | int main() 67 | { 68 | Block b; 69 | 70 | b=a; 71 | 72 | puts(b.data); 73 | return 0; 74 | } 75 | 76 | `  `Block a={{'a','b','c',}} 是对 a 的部分初始化, 77 | 'c' 后面自动填 0,写成 Block a={{"abc"}} 也一样, 78 | C 语言对初始化还是很宽容的。 79 | 80 |   上面这个程序居然正常的编译、运行了,这究竟是怎样的逆天? 81 | 看看汇编部分: 82 | 83 | leal 24(%esp), %edx 84 | movl $a, %ebx 85 | movl $250, %eax 86 | movl %edx, %edi # edi = &b 87 | movl %ebx, %esi # esi = &a 88 | movl %eax, %ecx # ecx = 250 89 | rep movsl 90 | 91 | `  `我们发现程序确实通过 250 次 movsl 复制了一个"数组"。 92 | 其原因是:结构体是可以复制的, 93 | 结构体又可以包括任意类型的子元素,数组也行, 94 | 所以"数组"也被复制了。 95 | 96 |   那为什么纯粹的数组就不能复制呢? 97 | 我们可以这样去理解:一个变量能被复制的必要条件是 98 | 我们知道它的大小。结构体做为自定义类型, 99 | 在编译的时候编译器必然存储了它的子元素类型、个数等相关信息, 100 | 结构体的大小也就知道了;而数组一般只在乎它的类型和 101 | 起始地址,元素个数总是被忽视的(例如: 102 | void func(char s[]) 可接受任何长度的字符数组做参数), 103 | 而且元素个数也没有被当做数组的一部分存入内存, 104 | 所以数组的复制是不好实现的。 105 | 106 | ## 小结 107 | 108 |   如果给结构体下一个实在点的定义话,那就是: 109 | 有格式的字节数组。有了结构体后 C 语言的 110 | 变量类型就丰富多了,但是同时也要注意: 111 | 112 | 1. 超过 4 字节的结构体不宜做参数(参数传递浪费时间、空间), 113 | 换做指针更好。 114 | 2. 超过 4 字节的结构体不宜做返回值类型 115 | (话说一般返回值都用 eax 来存, 116 | 那么超过 4 字节的时候怎么存呢?自己去探索吧!)。 117 | 118 | [回目录][content] 119 | -------------------------------------------------------------------------------- /varargs.md: -------------------------------------------------------------------------------- 1 | [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content 2 | 3 | [回目录][content] 4 | 5 | 6 | 7 |

可变参数 8 |

9 | 10 |    C语言的可变参数的实现非常巧妙: 11 | 大师只用了 3 个宏就解决了这个难题。 12 | 13 | ## 一、可变参数的应用 14 | 15 |   这里实现一个简单的可变参数函数 sum: 16 | 它将个数不定的多个整型参数求和后返回, 17 | 其第 1 个参数指明了要相加的数的个数(va.c): 18 | 19 | #include 20 | #include 21 | 22 | // 要相加的整数的个数为 n 23 | int sum(int n, ...) 24 | { 25 | va_list ap; 26 | va_start(ap, n); 27 | 28 | int ans = 0; 29 | while(n--) 30 | ans += va_arg(ap, int); 31 | 32 | va_end(ap); 33 | return ans; 34 | } 35 | 36 | int main() 37 | { 38 | int ans = sum(2, 3, 4); 39 | printf("%d\n", ans); 40 | 41 | return 0; 42 | } 43 | 44 | `  `sum 函数的第一个参数是 int n, 45 | 逗号后面是连续的 3 个英文句点, 46 | 表示参数 n 之后可以跟 0、1、2…… 个任意类型的参数。 47 | sum 可以这么用: 48 | 49 | sum(0); 50 | sum(1, 2); 51 | sum(3, 1, 1, 1); 52 | 53 | ## 二、可变参数的实现 54 | 55 |   可以看到在 sum 函数中用到了 3 个函数一样的东西: 56 | va\_start、va\_arg、va\_end, 57 | 它们是标准库(意味着各种平台都有)头文件 stdarg.h 中 58 | 定义的宏,这 3 个宏经过清理后是下面这个样子: 59 | 60 | typedef char* va_list; 61 | #define va_start(ap,v) ( ap = (va_list)(&v) + sizeof(v) ) 62 | #define va_arg(ap,t) ( *(t *)((ap += sizeof(t)) - sizeof(t)) ) 63 | #define va_end(ap) ( ap = NULL ) 64 | 65 | * va\_start 将 ap 定位到可变参数列表的起始地址 66 | * va\_arg 每次返回一个参数,并后移 ap 指针 67 | * va\_end 将 ap 置 NULL(避免非法使用) 68 | 69 | `  `这 3 个宏的实现就是基于 C语言默认调用惯例是从右至左 70 | 将参数压栈的事实,比如说 va.c 中调用 sum 函数, 71 | 参数压栈的顺序为:4->3->2, 72 | 又因为 x86 CPU 的栈是向低地址增长的, 73 | 所以参数的排列顺序如下: 74 | 75 | ![args](http://fmn.rrimg.com/fmn062/20121221/1930/original_qweH_1b90000008bb125c.jpg) 76 | 77 |   va\_start(n, ap) 78 | 就是 ( ap = (char*)(&n) + 4 ) 79 | 因此 ap 被赋值为 ebp+12 也就是变参列表的起始地址。 80 | 81 |   之后 va\_arg 取出每一个参数: 82 | ( *(int *)((ap += 4) - 4) ) 83 | 它首先将变参指针 ap 右移到下一个参数的起始地址, 84 | 再将加赋操作的返回值减到之前的位置取出一个参数。 85 | 这样,用一条语句既取出了当前参数,又后移了指针 ap, 86 | 真是神了! 87 | 88 |   sum 中循环使用 va\_arg 就取出了 n 个要相加的整数。 89 | 90 | ## 三、变参函数的可行性 91 | 92 |   一个变参函数能接受个数、类型可变的参数, 93 | 需要满足以下两个条件: 94 | 95 | 1. 能定位到可变参数列表的起始地址 96 | 2. 能获知可变参数的个数、每个参数的大小(类型) 97 | 98 | `  `条件 1 只要有个前置参数就能满足, 99 | 而对于这样的变参函数:void func(...); 100 | 编译能通过,但是不能用 va_start 取到变参列表的起始地址, 101 | 所以基本不可行。 102 | 103 |
104 | 105 |   sum 函数中参数 n 被用来定位可变参数列表的起始地址 106 | (满足条件1);n 的值是可变参数的个数, 107 | 类型默认全部是 int 型(满足条件2), 108 | 因此 sum 能正常工作。 109 | 110 |
111 | 112 |   再看看 printf 函数是如何满足以上两个条件的, 113 | printf 函数的原型是: 114 | 115 | int printf(const char *fmt, ...); 116 | 117 | `  `printf 的第1个参数 fmt(格式串)被用来定位其后 118 | 的可变参数的起始地址(满足条件1); 119 | fmt 指向的字符串中的各个格式描述符如:%d、%lf、%s 等 120 | 告诉了 printf fmt 之后参数的个数、各个参数的类型 121 | (满足条件2),因此 printf 能正常工作。 122 | 123 |
124 | 125 |   当然,sum、printf 能正常工作是设计者一厢情愿的期望, 126 | 如果使用者不按规矩传入参数、格式串,函数能正常工作才怪! 127 | 比如: 128 | 129 | sum(2, "111", "222"); 130 | printf("%s", 0); 131 | 132 | `  `编译器可不会进行可变参数的类型检查、格式串-参数匹配, 133 | 后果将会在运行的时候出现…… 134 | 135 | [回目录][content] 136 | --------------------------------------------------------------------------------