Repository: 1184893257/simplelinux Branch: master Commit: eb833374548a Files: 26 Total size: 58.6 KB Directory structure: gitextract_pxkvq6ay/ ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ 

朴素linux

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

内存对齐

## 为什么要进行内存对齐   在计算机组成原理中我们学到: 一块内存芯片一般只提供 8 位数据线,要进行 16 位数据的读写 可采用奇偶分体来组织管理多个芯片, 32 位也类似: ![align](http://fmn.rrimg.com/fmn056/20121121/1905/original_zCiC_1caa000049ee125c.jpg)   这样,连续的四个字节会分布在不同的芯片上, 送入地址 0,我们可将第 0、1、2、3 四个字节一次性读出组成 一个 32 位数,送入地址 4(每个芯片接收到的地址是1), 可一次性读出 4、5、6、7 四个字节。   但是如果要读 1、2、3、4 四个字节,就麻烦了, 有的 CPU 直接歇菜了:我处理不了! 但 Intel 的 CPU 走的是复杂指令集路线, 岂能就此认输,它通过两次内存读, 然后进行拼接合成我们想要的那个 32 位数, 而这一切是在比机器码更低级的微指令执行阶段完成的, 所以 movl 1, %eax 会不出意外地读出 1、2、3、4 四个字节 到 eax,证据如下(mem.c): #include char a[]={0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; int main() { int *p = (int*)(a + 1); int ans = *p; printf("*p:\t%p\n", ans); printf("a:\t%p\n", a); printf("p:\t%p\n", p); return 0; } `  `该程序的运行结果如下: [lqy@localhost temp]$ gcc -o mem mem.c [lqy@localhost temp]$ ./mem *p: 0x55443322 a: 0x80496a8 p: 0x80496a9 [lqy@localhost temp]$ `  `可看出程序确实从一个未对齐到 4 字节的地址(0x80496a9) 后读出了 4 个字节,从汇编可看出确实是 1 条 mov 指令读出来的: movl $a, %eax addl $1, %eax movl %eax, 28(%esp) # 初始化指针 p movl 28(%esp), %eax movl (%eax), %eax # 这里读出了 0x55443322 movl %eax, 24(%esp) # 初始化 ans `  `虽然 Intel 的 CPU 能这样处理,但还是要浪费点时间不是, 所以 C 程序还是要采取措施避免这种情况的发生, 那就是内存对齐。 ## 内存对齐的结果   内存对齐的完整描述你还是去百度吧, 这里我只是含糊地介绍一下: 1. 保证最大类型对齐到它的 size 2. 尽量不浪费空间 比如: struct A{ char a; int c; }; 它的大小为 8,c 的内部偏移为 4, 这样就可以一次性读出 c 了。 再如: struct B{ char a; char b; int c; }; 它的大小还是 8,第 2 条起作用了! ## 关闭内存对齐   讲到内存对齐,估计大家最期待的一大快事就是怎么关闭它 (默认是开启的),毕竟 Intel CPU 如此强大, 关闭了也没事。   关闭它也甚是简单,添加预处理指令 #pragma pack(1) 就行,windows linux 都管用: #include #pragma pack(1) struct _A{ char c; int i; }; //__attribute__((packed)); typedef struct _A A; int main() { printf("%d\n", sizeof(A)); return 0; } `  `linux gcc 中更常见的是使用 `__attribute__((packed))`, 这个属性只解除对一个结构体的内存对齐,而 #pragma pack(1) 解除了整个 C源文件 的内存对齐, 所以有时候 `__attribute__((packed))` 显得更为合理。   什么时候可能需要注意或者关闭内存对齐呢? 我想大概是这两种情况: * 结构化文件的读写 * 网络数据传输 ## 另一个浪费内存的家伙   说到内存对齐,我想起了另一个喜欢浪费内存的家伙: 参数对齐(我瞎编的名字,C 标准中或许有明确规定)。 看下面这个程序: #include typedef unsigned char u_char; u_char add(u_char a, u_char b) { return (u_char)(a+b); } int main() { u_char a=1, b=2; printf("ans:%d\n", add(a, b)); return 0; } `  `你说 add 函数的参数会占几个字节呢?2个?4个? 结果是 8 个……   “可恨”的是,这个家伙浪费内存的行为却被所有编译器纵容, 我们无法追究它的责任。 (应该是为了方便计算参数位置而规定的) [回目录][content] ================================================ FILE: array.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [Ŀ¼][content]

ָ

ָʲô Carray.c #include int main() { int date[3] = {2012,11,11}; int *p = date; int a = date[1]; int b = p[1]; printf("a:%d b:%d\n", a, b); printf("date:%p\np :%p\n", date, p); return 0; } ``༰עͣ
    .file	"array.c"
	.section	.rodata
.LC0:
	.string	"a:%d b:%d\n"
.LC1:
	.string	"date:%p\np   :%p\n"
	.text
.globl main
	.type	main, @function
main:
	pushl	%ebp			#-ָ֡л
	movl	%esp, %ebp		#/
	andl	$-16, %esp		#-ջ뵽16ֽ
	subl	$48, %esp		#-ؾֲռ
	movl	$2012, 24(%esp)	#\
	movl	$11, 28(%esp)	#-dateʼ
	movl	$11, 32(%esp)	#/
	leal	24(%esp), %eax	#\
	movl	%eax, 44(%esp)	#-ʼָp
	movl	28(%esp), %eax	#\
	movl	%eax, 40(%esp)	#-ʼa
	movl	44(%esp), %eax	#\
	addl	$4, %eax		#-\
	movl	(%eax), %eax	#-ʼb
	movl	%eax, 36(%esp)	#/
    movl    $.LC0, %eax
	movl	36(%esp), %edx
	movl	%edx, 8(%esp)
	movl	40(%esp), %edx
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	printf		#һε printf 
    movl    $.LC1, %eax
	movl	44(%esp), %edx
	movl	%edx, 8(%esp)
	leal	24(%esp), %edx
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	printf		#ڶε printf 
    movl    $0, %eax    #return 0
	leave
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 4.5.1 20100924 (Red Hat 4.5.1-4)"
	.section	.note.GNU-stack,"",@progbits
Ҷ lea ָܱȽİ mov ָ lea ָݵڴĵַ mov ָݵĵ ڴֵ磺 leal 24(%esp), %eax # 24+esp Ľ eax movl 24(%esp), %eax # 2012 eax ## ܽ ȣǿ main ҲһĻ з Double һĽṹ Σָд洢ռģ32λ4ֽڵĴС д洢һڴַֻеԪд洢ռ䣬 C оõ׵ַ棩 ˵ûд洢ռġ轫׵ֵַһָ룬 ׵ַĴʽǣ * ȫ飬 mov ָеһ * Ǿֲ飬 lea ָ esp ebp + Ľ ``׵ַһָIJ ھֲռУҲȫֱռУ DZ̻ڴ ˣ׵ַܱġ printf ĵøĺһȴ Ȼ call printfӡָ붼ʹ %p ʽ н£ [lqy@localhost temp]$ gcc -o array array.c [lqy@localhost temp]$ ./array a:11 b:11 date:0xbf9c3b68 p :0xbf9c3b68 [lqy@localhost temp]$ ``date p ֵ date ׵ַ [Ŀ¼][content] ================================================ FILE: bss.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

未初始化全局变量

  为下一篇介绍进程内存分布做准备, 这一篇先来介绍一下未初始化全局变量:   未初始化全局变量,这名字就很直白,就是 C 程序中定义成 全局作用域而又没有初始化的变量,我们知道这种变量在程序运行 后是被自动初始化为 全0 的。编译器编译的时候会将这类变量 收集起来集中放置到 .bss 段中,这个段只记录了段长, 没有实际上的内容(全是0,没必要存储), 在程序被装载时操作系统会 为它分配等于段长的内存,并全部初始化为0。   这有两个 C程序,都定义了全局数组 data(长度为1M, 占用内存4MB),一个部分初始化(bss\_init1.c), 一个未初始化(bss\_uninit1.c): bss_init1.c: #include #include #define MAXLEN 1024*1024 int data[MAXLEN]={1,}; int main() { Sleep(-1); return 0; } bss_uninit1.c: #include #include #define MAXLEN 1024*1024 int data[MAXLEN]; int main() { Sleep(-1); return 0; } `  `编译以上两个程序后: ![bss1](http://fmn.rrfmn.com/fmn059/20121203/1935/original_4q5M_35d80000b351118d.jpg)   可以看到有初始化的可执行文件的大小差不多是4MB, 而未初始化的只有47KB!这就是 .bss 段有段长, 而没有实际内容的表现。用 UltraEdit 打开 bss_init1.exe 可看到文件中大部分是全0(data数组的内容): ![bss5](http://fmn.rrimg.com/fmn065/20121203/1935/original_RbRN_5afd0000b341125d.jpg)   但是接下来运行(return 0 之前的 Sleep(-1) 保证了 程序暂时不会退出)的时候,却发现 bss_init1.exe 占用的空间明显少于 4MB,这是怎么回事呢? ![bss2](http://fmn.rrimg.com/fmn065/20121203/1935/original_ejt4_363e0000b309118d.jpg)   这就涉及程序装载的策略了。早期的操作系统(如:linux 0.01) 采用的是一次装载:将可执行文件一次性完整装入内存后再执行程序。 不管程序是 1KB 还是 60MB,都要等全部装入内存后才能执行, 这显然是不太合理的。   而现在的操作系统都是采用延迟装载: 将进程空间映射到可执行文件之后就开始执行了, 执行的时候如果发现要读/写的页不在内存中, 就根据映射关系去读取进来,然后继续执行应用程序 (应该是在页保护异常的处理中实现的)。   bss_init1.exe 肯定是被映射了,而程序中又没有对 data 数组进行读/写操作,所以操作系统也就懒得去装入这片内存了。 下面修改一下这两个程序:在 Sleep(-1) 前将 data 数组 的每个元素赋值为 -1: int i; for(i=0; i

ֵ

ںһƪѾҿֵݵࣺ ʵΡβиԵĴ洢ռ䣨ʵҲֻһֵ ûд洢ռ䣩ʵ -> βǸֵḶ́ ںǰ̣ ˺жβν޸ģʵεֵű䡣 Ǿû취ں޸ⲿˣ ָͺˣDzҪ޸ֵָ ֻ޸ָָڴ飺 ## 0ָ루͡ṹ壩 int add(int a, int b) { return a + b; } ``ֲֻĺֻҪֵм󷵻ؽ Ҫ޸ⲿ ## 1ָ void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; } ``ʹó int a=1, b=2; swap(&a, &b); ``ʹ 1 ָһĿģ 1. Ҫ޸ⲿ ṹ ֵ 2. Ϊ˽ʡռ躯Ҫһⲿ ռÿռܴĽṹ 4KB С Ȼûָĵַ ֻռ 4 ֽڣýṹΪ Ҫռ 4KB ڴ棬һҪֵ char* ַҲԿdzڽʡռĿġ ## 2ָ C ׼ͷļ string.h ṩһ strdup char *strdup(char *s); ``úǸַ صַĴ洢ռǶ̬ģԭַĿռ䡣 ǵ 1 ˵ҪԷƱ ҲǴ²֪O(_)O~ ڲ÷ֵ 2 ָʵ void my_strdup(char **p, char *s) { unsigned int len = strlen(s) + 1; *p = (char *)malloc(len); memcpy(*p, s, len); } ``ʹó char *s = "abc"; char *d = NULL; my_strdup(&d, s); ``пԿ 2 ָҪ޸ָʱ һҪںΪָ붯̬ռ䡣 һ÷ָʵ֣жָҪһ޸ģ 2 ָҪܶ࣬Ϊֵֻ 1 ĸ﷨ûƵġ strdup صַҪ free Ŷ ⣬һûʹ 3 ָıҪ [Ŀ¼][content] ================================================ FILE: call.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [Ŀ¼][content]

## ǰ д 顢ṹ塢ָ ֮дõģ ΪûǣЩ ҾĻܻ¶δţ Իȼ򵥵طһºðɣ ֮ٲϵƺһ ## CԴdouble.c #include int Double(int b) { int c; c = b + b; ++b; // Ӱ쵽 a return c; } int main() { int a = 1; int d = Double(a); printf("a:%d d:%d\n", a, d); return 0; } ## gcc -S double.c ``gcc -S double.c ĬϾǰѻԴ double.s У ֮ǰһֱ -o ѡΪ˱ĽO(_)O~ double.s еݼ򻯺
Double:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$16, %esp
	movl	8(%ebp), %eax
	addl	%eax, %eax
	movl	%eax, -4(%ebp)
	addl	$1, 8(%ebp)
	movl	-4(%ebp), %eax
	leave
	ret

main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	movl	$1, 28(%esp)
	movl	28(%esp), %eax
	movl	%eax, (%esp)
	call	Double
	movl	%eax, 24(%esp)
	movl	$.LC0, %eax
	movl	24(%esp), %edx
	movl	%edx, 8(%esp)
	movl	28(%esp), %edx
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	printf
	movl	$0, %eax
	leave
	ret
## Ƕ Double һá ڴ main е 1 ָʼ

	movl	$1, 28(%esp)	# a=1
Ǹ ֲ a ֵ
ջָĴ esp ڵֵ 8000 ִָջͼ

	movl	28(%esp), %eax
	movl	%eax, (%esp)	# (%esp) = a
ָ a ֵ 1 д˵ַΪ 8000 ڴ飨4ֽڣ b ɣ
ڻҪ½ۡ

	call	Double
call ִָУɷΪ裺
  1. call ָһָĵַѹջ
  2. eip ޸Ϊ Double ĵַ
Ȼת Double ȡִָˡ
call ָ֮ movl %eax, 24(%esp) ĵַ 1000ô call ִк ջΪͼ

	pushl	%ebp
	movl	%esp, %ebp
Ƚɵ ebp ѹջȻʱ esp ֵ ebp ῴĿ

	subl	$16, %esp	# esp -= 16
Double ľֲռôˣȻе˷ѣ

	movl	8(%ebp), %eax
	addl	%eax, %eax
	movl	%eax, -4(%ebp)	# c = b + b
	addl	$1, 8(%ebp)		# ++b
8(%ebp) ǰպñʾ 8000 ڴ飬 4 ָ 8000 ڴֵеĴ ǿȷ 8000 Dz b -4(%ebp) c

	movl	-4(%ebp), %eax	# eax = c
	leave
	ret
βˣķֵҪ洢ۼӼĴ eax leave ָͬ
movl %ebp, %esp
popl %ebp
ս Double ʱָβӦ
Ǿֲռ䱻ˣebp Ҳԭˡ

ret ൱ popl %eip ͼִ call ָָ֮ˡ

Ȼ Double ľֲռ䱻ˣ еֵDZֲģһֱ֮ printf ʱ Double ľֲԼݲűǡ
## һķٴӴϻعһ Double
Double:
	pushl	%ebp				#----------ָ֡ ebp л
	movl	%esp, %ebp			#---------/
	subl	$16, %esp			#----------ؾֲռ
	movl	8(%ebp), %eax		#\
	addl	%eax, %eax			#-\
	movl	%eax, -4(%ebp)		#-C IJ
	addl	$1, 8(%ebp)			#/
	movl	-4(%ebp), %eax		#-----ֵ浽 eax Ĵ
	leave						#--------\
	ret							#---------ջָָ֡롢
ΪָҲл֡ Double ̫򵥣ûгֱĴָ eax ĴҪÿ֪淵ֵģ ں󲿷ֿ϶ᱻ޸ģӵĺںǰ pushl ޸ĵļĴ ret ֮ǰ popl ĴԻָԭֵ ## С ͨ Double ķ ע⵽ ebp esp ƣ оҲͨ ebp + ƫ ķʽ ֲ ڿҳŵˣesp ebp ƫƾ ֲıʽ ָ֡Ĵ ebp ٳһƪз ͬʱҲԵؿֵݵḶ́ ǰ a Ƶ bֵǷֱDzͬڴ飩 b ֱӱˣҲٿ a Ȼ Double ++b ˣ a ֵȻΪ 1 н£ [lqy@localhost temp]$ gcc -o double double.c [lqy@localhost temp]$ ./double a:1 d:2 [lqy@localhost temp]$ ``һƪټֵݡ [Ŀ¼][content] ================================================ FILE: dynamicstack.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

汇编实现的动态栈

  这一篇就是实现 d_printf,废话不多说,直接上代码。 由于 VC 的内联汇编还是比较清晰,那就先贴 VC 版的。 ## 一、d_printf VC版 #include void d_printf(const char *fmt, int n, int a[]) { static int size1, size2; static const char *fmt_copy; size1 = 4*n; // 可变参数的空间大小 size2 = size1 + 4; // 还有 fmt 指针4字节, 恢复 esp 时用 fmt_copy = fmt; __asm{ // 保护要修改的 ecx/esi/edi 寄存器 push ecx push esi push edi // 给 ecx/esi/edi 赋值 mov ecx, n // movsd 的执行次数 mov esi, a // a -> esi sub esp, size1 mov edi, esp // esp - size1 -> edi rep movsd // n 次4字节拷贝 push fmt_copy // 压栈格式串(字符串指针) call printf add esp, size2 // 恢复栈 // 恢复各个寄存器 pop edi pop esi pop ecx } } int main() { char fmt[1024]; // 格式串 char c; // 额外读取一个字符 int a[1024]; // 存读到的整数 int i; while(EOF != scanf("%s%c", fmt, &c)) // 读到 EOF 就结束 { if(c == '\n') // 格式串后没有数字 { printf("%s\n\n", fmt); // 直接打印, 不用 d_printf continue; } // 循环读取各个整数 i = 0; do { scanf("%d%c", &a[i++], &c); }while(c != '\n'); // 调用 d_printf, i 刚好是输入的整数的个数 d_printf(fmt, i, a); printf("\n\n"); // 补个换行比较好看O(∩_∩)O~ } } ## 二、d_printf gcc 版(main 函数跟 VC 版的一样) #include void d_printf(const char *fmt, int n, int a[]) { int d0, d1, d2; static int size1, size2; static const char *fmt_copy; size1 = 4*n; // 可变参数的空间大小 size2 = size1 + 4; // 还有 fmt 指针4字节, 恢复 esp 时用 fmt_copy = fmt; asm volatile( "subl %6, %%esp\n\t" "movl %%esp, %%edi\n\t" "rep ; movsl\n\t" "pushl %3\n\t" "call printf\n\t" "addl %7, %%esp" : "=&S"(d0), "=&D"(d1), "=&c"(d2) : "m"(fmt_copy), "0"(a), "2"(n), "m"(size1), "m"(size2)); } int main() { char fmt[1024]; // 格式串 char c; // 额外读取一个字符 int a[1024]; // 存读到的整数 int i; while(EOF != scanf("%s%c", fmt, &c)) // 读到 EOF 就结束 { if(c == '\n') // 格式串后没有数字 { printf("%s\n\n", fmt); // 直接打印, 不用 d_printf continue; } // 循环读取各个整数 i = 0; do { scanf("%d%c", &a[i++], &c); }while(c != '\n'); // 调用 d_printf, i 刚好是输入的整数的个数 d_printf(fmt, i, a); printf("\n\n"); // 补个换行比较好看O(∩_∩)O~ } } ## 三、运行效果   linux 中的运行效果如下: [lqy@localhost temp]$ ./d_printf nospaceword nospaceword %X 256 100 %d+%d=%d 1 2 3 1+2=3 >%3d>%03d> 3 3 > 3>003> >%3d>%-3d> 3 3 > 3>3 > [lqy@localhost temp]$ `  `最后输入 EOF 结束:Ctrl + D(linux)、Ctrl + Z (windows)。由于转义符是编译时处理的,printf 是不管的, 所以\n什么的在这里不管用^_^。 ## 四、为什么用 static   d\_printf 中为什么将 size1、size2、fmt_copy 声明为 static 变量呢? 1. 不能用寄存器。size2 是在 call printf 之后使用的, 但是后来我发现 printf 执行完后改动了好几个寄存器的值, 所以如果将 size2 保存到某个寄存器中是不行的 (难怪C语言喜欢内存,寄存器太不可靠了o(╯□╰)o)。 2. 不能用局部变量。不让用寄存器就用内存呗! 但是我们要自主修改 esp, 而局部变量有可能是通过 esp+常量偏移 定位的 (如果是用 ebp+常量偏移(VC一般这么用)定位的就没问题), 所以 subl %6, %%esp 之后 到 addl %7, %%esp 之前 都不能使用局部变量,否则会定位错误。 所以才使用 static 变量, 因为 static 变量是用绝对地址定位的,跟 esp 毫无关系。 ## 五、使用内联汇编的建议 1. 不要用。 2. 如果非得用的话,尽量用 C 语言实现+-*/, 如 d_printf 中给 size1、size2 赋值; 内联汇编只实现不得不用的部分(内联汇编一般都短小精悍)。

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

[回目录][content] ================================================ FILE: frame.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

函数帧

  这标题一念出来我立刻想到了一个名人:白素贞……当然, 此女与本文无关,下面进入正题:
其实程序运行就好比一帧一帧地放电影,每一帧是一次函数调用,电影放完了,我们就看到结局了。
  我们用一个递归求解阶乘的程序来看看这个放映过程(fac.c): #include int fac(int n) { if(n <= 1) return 1; return n * fac(n-1); } int main() { int n = 3; int ans = fac(n); printf("%d! = %d\n", n, ans); return 0; } ## main 帧   首先 main 函数被调用(程序可不是从 main 开始执行的):
main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	movl	$3, 28(%esp)	# n = 3
	movl	28(%esp), %eax
	movl	%eax, (%esp)
	call	fac
	movl	%eax, 24(%esp)	# 返回值存入 ans
	movl	$.LC0, %eax
	movl	24(%esp), %edx
	movl	%edx, 8(%esp)
	movl	28(%esp), %edx
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	printf
	movl	$0, %eax
	leave
	ret
`  `main 函数创建了一帧: * 从 esp 到 ebp + 4 * 上边是本次调用的返回地址、旧的 ebp 指针 * 然后是 main 的局部变量 n、ans * 最下边是参数的空间,右上图显示的是 main 中调用 printf 前的栈的使用情况 `  `进入 main 函数,前 4 条指令开辟了这片空间, 在退出 main 函数之前的 leave ret 回收了这片空间 (C++ 在回收这片空间之前要析构此函数中的所有局部对象)。 在 main 函数执行期间 ebp 一直指向 帧顶 - 4 的位置, ebp 被称为帧指针也就是这个原因。 ## 调用惯例   调用函数的时候,先传参数,然后 call, 具体这个过程怎么实现有相关规定,这样的规定被称为调用惯例, C语言中有多种调用惯例,它们的不同之处在于: 1. 参数是压栈还是存入寄存器 2. 参数压栈的次序(从右至左 | 从左至右) 3. 调用完成后是调用者还是被调用者来恢复栈 `  `各种调用惯例《程序员的自我修养》——链接、装载与库 这本书中有简要介绍,我照抄后在本文后面列出。C语言默认的 调用惯例是 cdecl: 1. 参数从右至左压栈 2. 调用完成后调用者负责恢复栈 `  `可以从 printf("%d! = %d\n", n, ans); 的调用过程 中看出。   虽然 VC、gcc 都默认使用 cdecl 调用惯例, 但它们的实现却各有风格: * VC 一般是从右至左 push 参数,call,add esp, XXX * 而 gcc 在给局部变量分配空间的时候也给参数分配了足够的空间, 所以只要从右至左 mov 参数, XXX(%esp),call 就可以了, 调用者根本不用去恢复栈,因为传参数的时候并没有修改栈指针 esp。 ## fac 帧   说完调用惯例我们接着来看第一次调用 fac:
fac:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	cmpl	$1, 8(%ebp)
	jg	.L2			# n > 1 就跳到 .L2
	movl	$1, %eax
	jmp	.L3			# 无条件跳到 .L3
.L2:
	movl	8(%ebp), %eax
	subl	$1, %eax
	movl	%eax, (%esp)
	call	fac		#  fac(n-1)
	imull	8(%ebp), %eax	# eax = n * eax
.L3:
	leave
	ret
  fac(3) 开辟了第一个 fac 帧: * 从 esp 到 ebp + 4(fac 还能"越界"地读到参数 n) * 上边是 返回地址、旧的 ebp 指针(指向 main 帧) * fac 没有局部变量,又浪费了很多字节 * 参数占了最下边的 4 字节(需要递归时使用) `  `这时还不满足递归终止条件,于是fac(3)又递归地调用了fac(2), fac(2)又递归的调用了fac(1),到这个时候栈变成了如下情况: ![total](http://fmn.rrimg.com/fmn062/20121124/1940/original_y9zg_1a3800005b5a118e.jpg)   上图的箭头的含义很明显: 从 ebp 可回溯到所有的函数帧, 这是由于每个函数开头都来两条 pushl %ebp、movl %esp, %ebp造成的。   参数总是调用者写入,被调用者来读取(被调用者修改参数毫无意义), 这是一种默契^_^。 程序继续运行: 1. fac(1) 满足了递归终止条件,fac(1) 返回 1,fac(1)#3 帧消亡 2. 继续执行 fac(2),fac(2) 返回 1\*2,fac(2)#2 帧消亡 3. 继续执行 fac(3),fac(3) 返回 2\*3,fac(1)#1 帧消亡 4. 继续执行 main,printf 结果,返回 0,main 帧消亡 5. 继续执行 ???(且听下回分解) 最终程序结束(进程僵死,一会儿后操作系统会来收尸 (回收内存及其他资源))。 ## 小结   函数帧保存的是函数的一个完整的局部环境, 保证了函数调用的正确返回(函数帧中有返回地址)、 返回后继续正确地执行,因此函数帧是 C语言 能调来调去的保障。

主要的调用惯例

调用惯例 出栈方 参数传递 名字修饰
cdecl 函数调用方 从右至左的顺序压参数入栈 下划线+函数名
stdcall 函数本身 从右至左的顺序压参数入栈 下划线+函数名+@+参数的字节数, 如函数 int func(int a, double b)的修饰名是 _func@12
fastcall 函数本身 头两个 DWORD(4字节)类型或者更少字节的参数 被放入寄存器,其他剩下的参数按从右至左的顺序入栈 @+函数名+@+参数的字节数
pascal 函数本身 从左至右的顺序入栈 较为复杂,参见pascal文档
[回目录][content] ================================================ FILE: gcc.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [Ŀ¼][content]

ͻ۽

linux ±д C ô㽫Ϭķ --- ## һCmax.c #define MAX(a,b) ((a)>=(b)?(a):(b)) int main(){ int c=MAX(1,2); // עעעע return 0; } ``ܼ򵥣ǶʹһMAX꣬ ʽǰǻᱻ滻ΪĿģ ڿIJգ gcc -E -o max2.c max.c `` -o max2.c gcc Ҫ max2.c ļ ֣ΰɣ # 1 "max.c" # 1 "" # 1 "<>" # 1 "max.c" int main(){ int c=((1)>=(2)?(1):(2)); return 0; } ``max2.cеݣMAX(1,2) 滻 ((1)>=(2)?(1):(2))ֻˣ þ滻꣬ǺҶ̫á ִ linux ںԴмֱõ˼£ ˵ linux ں Cꡢ дġ ǿǶ׵ģҲ˵ Ҳ лԳܹ滻ĺ꣬ ൱ˡʮַļ򵥵һ䣬 ԭΪĿʱܾͱ߰ʮַˣ Ҫ䣬ʹˡ ں꣬һƪܡ --- ## ۽ ӦDz۽𾦵ģ ۽𾦿Կ΢Сϸڡд˸Hello World hello.c #include int main(){ printf("Hello, World!\n"); return 0; } ``Hello World Ͳý˰ɣO(_)O~ Ȼû۽һ£ gcc -S -o hello.s hello.c ``Hello World Ļͳ(hello.s) .file "hello.c" .section .rodata .LC0: .string "Hello, World!" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $16, %esp movl $.LC0, (%esp) call puts movl $0, %eax leave ret .size main, .-main .ident "GCC: (GNU) 4.5.1 20100924 (Red Hat 4.5.1-4)" .section .note.GNU-stack,"",@progbits ``Ǻ ֻҪ֪ôá۽𾦡ˣ ļƪÿˡ --- ͻ۽ʵǿضϱ õмģgccǣ Ԥ->->-> ``ʹòͬıѡԵóͬм
׶ ضϺIJ
Ԥ gcc -E 滻˺CԴ(û#define,#include), ɾע
gcc -S Դ
gcc -c Ŀļļ вڴļеⲿ
gcc ִгһɶĿļӶɣ ļбҵõ
Ҳͬѧ -c һûأ ļķҲõǺ٣õʱ˵ɡ [Ŀ¼][content] ================================================ FILE: globalvar.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [Ŀ¼][content]

ȫֱ

## ʵƷ Сglobal.c #include int i = 1; int main() { ++i; printf("%d\n",i); return 0; } ``i Ķ屻˺ߣ i ͳΪȫֱеĽҲģ ҹĵ i ʲô ## յĻ۽Ҳˣǻῴ ǵ÷ִļܿս [lqy@localhost temp]$ gcc -o global global.c [lqy@localhost temp]$ objdump -s -d global > global.txt [lqy@localhost temp]$ * *gcc -o global global.c* DZ global.c ɿִļ global * *objdump -s -d global > global.txt* Ƿ global * -s ԽжεʮƵķʽӡ * -d ԽаָĶη * > global.txt ǽ׼ global.txt ļ רҵĻ"ض" objdump linux һ๤ߣ ܹĿļִļ global ļķ global.txt ļУ global.txtļȽϳҵ 357 У λ main
080483c4 <main>:
 80483c4:	55                   	push   %ebp
 80483c5:	89 e5                	mov    %esp,%ebp
 80483c7:	83 e4 f0             	and    $0xfffffff0,%esp
 80483ca:	83 ec 10             	sub    $0x10,%esp
 80483cd:	a1 64 96 04 08       	mov    0x8049664,%eax
 80483d2:	83 c0 01             	add    $0x1,%eax
 80483d5:	a3 64 96 04 08       	mov    %eax,0x8049664
 80483da:	8b 15 64 96 04 08    	mov    0x8049664,%edx
 80483e0:	b8 c4 84 04 08       	mov    $0x80484c4,%eax
 80483e5:	89 54 24 04          	mov    %edx,0x4(%esp)
 80483e9:	89 04 24             	mov    %eax,(%esp)
 80483ec:	e8 03 ff ff ff       	call   80482f4 
 80483f1:	b8 00 00 00 00       	mov    $0x0,%eax
 80483f6:	c9                   	leave
 80483f7:	c3                   	ret
ֱ ++i ķ ȫֱ i ˾ԵַΪ 0x8049664 ڴ飨СΪ4ֽڣ ע 0x8049664 ָ 0x8049664 ҪʾֵΪ 0x8049664 ӦдΪ $0x8049664 ## յ ͨ۽𾦣 gcc -S -o global.s global.c ``ǿ ++i Ļʽǣ movl i, %eax addl $1, %eax movl %eax, i ``գ̫ʧˣ ## Ŀļеȫֱ ĿļҲ objdump ࣺ [lqy@localhost temp]$ gcc -c -o global.o global.c [lqy@localhost temp]$ objdump -s -d global.o > global.txt [lqy@localhost temp]$ ``global.o global.txt ûôˣ ֻ 35 У ++i ֵĽǣ 9: a1 00 00 00 00 mov 0x0,%eax e: 83 c0 01 add $0x1,%eax 11: a3 00 00 00 00 mov %eax,0x0 ``ôô 0x8049664 أ Ŀļ ִļˣ ȫֱĿļֻһðƵַ Ӻ󣨸ĿļЭ̣ΪԼȫֱһ̣ ϣյľԵַ ## ĿļͿִļ ĿļDZIJ Ѿ C ת DzֻIJҪӹ ִļɶĿļ C пӶɵģ C пṩ printf Ⱥʵ֡ linux ĿļĬչ.oͿִļ ELF ʽļݰһʽ֯Ķļ ƵģWindows VISUAL C++ Ŀļ չ.obj COFF ʽִļ չ.exe PE ʽ ELF PE Ǵ COFF չġ Ϊ linux ĿļͿִļݸʽһģ objdump ȿԷִļҲԷĿļ ## С ȫֱҲյڴַˣ һdz ֽһߣobjdump кҪõĹߺʹ÷ˣgccobjdump š񻹲ҪĶO(_)O~ [Ŀ¼][content] ================================================ FILE: inlineasm.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

内联汇编

  内联汇编是指在 C/C++ 代码中嵌入的汇编代码, 与全部是汇编的汇编源文件不同,它们被嵌入到 C/C++ 的大环境中。 ## 一、gcc 内联汇编   gcc 内联汇编的格式如下: asm ( 汇编语句 : 输出操作数 // 非必需 : 输入操作数 // 非必需 : 其他被污染的寄存器 // 非必需 ); `  `我们通过一个简单的例子来了解一下它的格式(gcc_add.c): #include int main() { int a=1, b=2, c=0; // 蛋疼的 add 操作 asm( "addl %2, %0" // 1 : "=g"(c) // 2 : "0"(a), "g"(b) // 3 : "memory"); // 4 printf("现在c是:%d\n", c); return 0; } `  `内联汇编中: 1. 第1行是汇编语句,用双引号引起来, 多条语句用 ; 或者 \n\t 来分隔。 2. 第2行是输出操作数,都是 "=?"(var) 的形式, var 可以是任意内存变量(输出结果会存到这个变量中), ? 一般是下面这些标识符 (表示内联汇编中用什么来代理这个操作数): * a,b,c,d,S,D 分别代表 eax,ebx,ecx,edx,esi,edi 寄存器 * r 上面的寄存器的任意一个(谁闲着就用谁) * m 内存 * i 立即数(常量,只用于输入操作数) * g 寄存器、内存、立即数 都行(gcc你看着办) 在汇编中用 %序号 来代表这些输入/输出操作数, 序号从 0 开始。为了与操作数区分开来, 寄存器用两个%引出,如:%%eax 3. 第3行是输入操作数,都是 "?"(var) 的形式, ? 除了可以是上面的那些标识符,还可以是输出操作数的序号, 表示用 var 来初始化该输出操作数, 上面的程序中 %0 和 %1 就是一个东西,初始化为 1(a的值)。 4. 第4行标出那些在汇编代码中修改了的、 又没有在输入/输出列表中列出的寄存器, 这样 gcc 就不会擅自使用这些"危险的"寄存器。 还可以用 "memory" 表示在内联汇编中修改了内存, 之前缓存在寄存器中的内存变量需要重新读取。 `  `上面这一段内联汇编的效果就是, 把a与b的和存入了c。当然这只是一个示例程序, 谁要真这么用就蛋疼了, 内联汇编一般在不得不用的情况下才使用。 ## 二、VC 内联汇编   gcc 内联汇编被设计得很复杂,初学者看了往往头大, 而 VC 的内联汇编就简单多了: __asm{ 汇编语句 } `  `一个例子程序如下(vc_add.c): #include int main() { int a=1, b=2, c=0; // 蛋疼的 add 操作 __asm{ push eax // 保护 eax mov eax, a // eax = a; add eax, b // eax = eax + b; mov c, eax // c = eax; pop eax // 恢复 eax } printf("现在c是:%d\n", c); return 0; } `  `VC 的内联汇编中可以直接以变量名的形式使用局部变量, 这就方便多了。但是, VC 内联汇编中有些变量名是保留的,比如:size, 使用这些变量名就会报错(把b改成size, 上面的程序就编译不通过了)。所以,起名字一定要小心!   因为 VC 没有输入/输出操作数列表, 它也不看你的汇编代码(直接拿去用), 所以它不知道你修改了哪些寄存器, 这些要修改的寄存器可能保存着重要数据, 所以用 push/pop 来 保护/恢复 要修改的寄存器。 而 gcc 就不需要,它能从输入/输出列表中获得丰富的信息 来调剂各个寄存器的使用, 并进行优化,所以从效率上说 VC 完败! ## 三、为什么用内联汇编   用内联汇编的主要目的是为了提高效率: 假设有一个比较文本差异的程序 diff, 它花了 99% 的时间在 strcmp 这个函数上, 如果用内联汇编实现的一个高效的 strcmp 比用 C 语言实现的快 1 倍,那么专家花在这个小小函数上的心思就能够将整个程序的效率 提高差不多 1 倍,这是很值得去做的"斤斤计较"。   还有一个目的就是为了实现 C 语言无法实现的部分, 比如说 IO 操作,还有我们上一篇中提到的自主修改 esp 寄存器 也是必须用汇编才能实现的。 ## 四、memcpy   学 gcc 内联汇编最好的导师莫过于 linux 内核, 有很多常用的小函数如 memcpy、strlen、strcpy、…… 其中都有短小精悍的内联汇编版本, 如在 linux 2.6.37 中的 memcpy 函数: // 位于 /arch/x86/boot/compressed/misc.c void *memcpy(void *dest, const void *src, size_t n) { int d0, d1, d2; asm volatile( "rep ; movsl\n\t" "movl %4,%%ecx\n\t" "rep ; movsb\n\t" : "=&c" (d0), "=&D" (d1), "=&S" (d2) : "0" (n >> 2), "g" (n & 3), "1" (dest), "2" (src) : "memory"); return dest; } `  `与 gcc_add.c 相比,这个函数要复杂不少: * 关键字 volatile 是告诉 gcc 不要尝试去移动、 删除这段内联汇编。 * rep ; movsl 的工作流程如下: while(ecx) { movl (%esi), (%edi); esi += 4; edi += 4; ecx--; } rep ; movsb 与此类似,只是每次拷贝的不是双字(4字节), 而是字节。 * "=&D" (d1) 不是想将 edi 的最终值输出到 d1 中, 而是想告诉 gcc edi的值早就改了, 不要认为它的值还是初始化时的 dest, 避免"吝啬的" gcc 把修改了的 edi 还当做 dest 来用。 而 d0、d1、d2 在开启优化后会被 gcc 无视掉 (输出到它们的值没有被用过)。 `  `memcpy 先复制一个一个的双字, 到最后如果还有没复制完的(少于4个字节), 再一个一个字节地复制。 我最终实现的 d_printf 就模仿了这个函数。 深入研究:
gcc 内联汇编 HOWTO 文档
Linux Cross Reference——各版本 linux 内核函数检索 [回目录][content] ================================================ FILE: localvar.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [Ŀ¼][content]

ֲ

ڽļƪУ ҽ"۽"һC Ϊҽҿ C Եİء ## һ죬һѷһֵ C int i = 3; int ans = (++i)+(++i)+(++i); ``˵ 18һΪ 4+5+6=15 ء ## ֤ ȻҾ֤һ½ģ дµIJԳinc.c #include int main() { int i = 3; int ans = (++i)+(++i)+(++i); printf("%d\n",ans); return 0; } `` linux б롢У£ [lqy@localhost temp]$ gcc -o inc inc.c [lqy@localhost temp]$ ./inc 16 [lqy@localhost temp]$ ``Ȼֳһ˷˼ 16 ## ðɣȲ 18 ˣ 16 ôģ gcc -S -o inc.s inc.c ``˻Դļ inc.s ֻе֣
    .file	"inc.c"
	.section	.rodata
.LC0:
	.string	"%d\n"
	.text
.globl main
	.type	main, @function
main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	movl	$3, 28(%esp)
	addl	$1, 28(%esp)
	addl	$1, 28(%esp)
	movl	28(%esp), %eax
	addl	%eax, %eax
	addl	$1, 28(%esp)
	addl	28(%esp), %eax
	movl	%eax, 24(%esp)
	movl	$.LC0, %eax
	movl	24(%esp), %edx
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	printf
	movl	$0, %eax
	leave
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 4.5.1 20100924 (Red Hat 4.5.1-4)"
	.section	.note.GNU-stack,"",@progbits
Ҳòһֻĸʽˣ AT&T ʽ x86 ࣬ windows ϼһ Intel ʽĻ࣬ AT&T Intel ʽĻЩ죬 Ǻܺġ ǼĴʽͬ Intel ʽļĴǰ˸ %eax %eax Ȼ˫ԪָIJĴݷ Intel ĸպ෴ mov eaxebx൱ eax=ebx; movl %ebx%eax Ҵֵ һЩ𣬲׿׵ġ 岿ּӵעͰɣ movl $3, 28(%esp) # i = 3; addl $1, 28(%esp) # ++i; // 4 addl $1, 28(%esp) # ++i; // 5 movl 28(%esp), %eax # eax = i; addl %eax, %eax # eax = eax + eax; // 10 addl $1, 28(%esp) # ++i; // 6 addl 28(%esp), %eax # eax = eax + i; // 16 movl %eax, 24(%esp) # ans = eax; ``ȻǾ֪ 16 ôˡ ΪʲôҾͿ϶ 28(%esp) DZ i أ Ϊֻд 3ûбڴ汻д 3 Ҵ֮ĸָҲȷ C еı iñ߰ һֲյԼĴѰַһڴ棡 Ƶģֲ ans 24(%esp) óֲ󶼱 esp Ѱַڴ ֮ƪ»ῴۻǺȷ ## ͬij VC ϱУ Debug ģʽн 16Release ģʽн 18 Visual Studio 2010 Debug Release ģʽ¶ 18 VC VS ҲԿ룬 ڵԹϵжϵʱ VC ʹ Alt + 8 򿪷ര壬 VS һ C Դ༭ѡ"ת"򿪷ര塣 Ϊʲô Intel Լ CPUAT&T һ Intel ͬʽĻأûӣAT&T ҲǺǵģ ˼Ӱȫ磺UnixCԡ ## С Ӧȡ飺 ʵʩݼıӦڱʽжγ ͲǿˣDZɷӣ C ׼涨[е](http://blog.csdn.net/huiguixian/article/details/6438613)֮䣬 ִе˳ġ ˱ŻĿռ䡣[[1]](#tip1) õ 15 ĻԸij #include int main() { int i = 3; int ans = (i+2)*3; i += 3; printf("%d\n",ans); return 0; } `` windows » linux £ Release Debugһ 15 C ԣųƪѾܹΪǽˣ պDZԪô [Ŀ¼][content] [1] ohyeah ָ[http://rs.xidian.edu.cn/forum.php?mod=redirect&goto=findpost&ptid=412474&pid=8298351](http://rs.xidian.edu.cn/forum.php?mod=redirect&goto=findpost&ptid=412474&pid=8298351) ================================================ FILE: macro.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

奇怪的宏

  这一篇介绍这些奇怪的宏: ## 一、do while(0)   为了交换两个整型变量的值,前面值传递中已经用 包含指针参数的 swap 函数做到了,这次用来实现(swap.c): #include #define SWAP(a,b) \ do{ \ int t = a; \ a = b; \ b = t; \ }while(0) int main() { int c=1, d=2; int t; // 测试 SWAP 与环境的兼容性 SWAP(c,d); printf("c:%d d:%d\n", c, d); return 0; } `  `这个宏看起来就有点怪了:do while(0) 是写了个循环 又不让它循环,蛋疼啊!其实不然,这样写是有妙用的:   首先,SWAP 有多条语句,如果这样写: #define SWAP(a,b) \ int t = a; \ a = b; \ b = t; `  `那么用的时候就得这么用: SWAP(c,d) `  `不能加分号!不习惯吧?   其次,使用 do{...}while(0), 中间的语句用大括号括起来了,所以是另一个命名空间, 其中的新变量 t 不会发生命名冲突。   SWAP 宏要比之前那个函数的效率要高, 因为没有发生函数调用,没有参数传递, 宏会在编译前被替换,所以只是嵌入了一小段代码。 ## 二、#   标题我没打错,这里要说的就是井号,#的功能是将其后面的 宏参数进行字符串化操作。比如下面代码中的宏: #define WARN_IF(EXP) \ do{ if (EXP) \ fprintf(stderr, "Warning: " #EXP "\n"); } \ while(0) `  `那么实际使用中会出现下面所示的替换过程: WARN_IF (divider == 0); `  `被替换为 do { if (divider == 0) fprintf(stderr, "Warning: " "divider == 0" "\n"); } while(0); `  `需要注意的是C语言中多个双引号字符串放在一起 会自动连接起来,所以如果 divider 为 0 的话,就会打印出: Warning: divider == 0 ## 三、##   # 还是比较少用的,## 却比较流行, 在 linux0.01 中就用到过。## 被称为连接符, 用来将两个 记号(编译原理中的词汇) 连接为一个 记号。 看下面的例子吧(add.c): #include #define add(Type) \ Type add##Type(Type a, Type b){ \ return a+b; \ } // 下面两条是奇迹发生的地方 add(int) add(double) int main() { int a = addint(1, 2); double d = adddouble(1.5, 1.5); printf("a:%d d:%lf\n", a, d); return 0; } `  `那两行被替换后是这个样子的: int addint(int a, int b){ return a+b; } double adddouble(double a, double b){ return a+b; } 以上内容都可以使用照妖镜看到宏被替换后的情形。 [回目录][content] ================================================ FILE: main.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

谁调用了main?

  这是函数帧的应用之一。 ## 操作可行性   从上一篇中可以发现:用帧指针 ebp 可以回溯到所有的函数帧, 那么 main 函数帧之上的函数帧自然也是可以的; 而帧中 旧ebp 的上一个四字节存的是函数的返回地址, 由这个地址我们可以判断出谁调用了这个函数。 ## 准备活动   下面就是这次黑客行动的主角(up.c): #include int main() { int *p; // 以下这行内联汇编将 ebp 寄存器的值存到指针 p 中 __asm__("movl %%ebp, %0" :"=m"(p)); while(p != NULL){ printf("%p\n", p[1]); p = (int*)(p[0]); } return 0; } `  `首先,请允许我使用一下 gcc 内联汇编, 这里简单的解释一下: 1. "=m"(p) 表示将内存变量 p 作为一个输出操作数 2. %0 代表的是第一个操作数,那就是 p 了 3. 为了与操作数区别开来,寄存器要多加个 %, %%ebp 表示的就是 ebp 寄存器 `  `总之,这块内联汇编将 ebp 寄存器的值赋给了指针 p。   然后解释一下while循环:循环中,首先打印 p[1], p[1]就是该帧所存的返回地址;然后将指针 p 改为 p[0], p[0]是 旧ebp(上一帧的帧指针); 这样,程序将按照调用顺序的逆序打印出各个返回地址。   为什么终止条件是 p==NULL 呢?这是 gcc 为了支援我们的 黑客行动特意在开始执行程序的时候将 ebp 清零了, 所以第一次执行某个函数的时候压栈的 旧ebp 是 NULL。 ## 开始行动   我们使用静态链接的方式编译 up.c (静态链接的可执行文件中包含所有用户态下执行的代码), 然后执行它: [lqy@localhost temp]$ gcc -static -o up up.c [lqy@localhost temp]$ ./up 0x8048464 0x80481e1 [lqy@localhost temp]$ ## 分析结果   up 打印了了两个指向代码区的地址, 接着就看它们是属于哪两个函数了: nm up | sort > up.txt * nm up 可列出各个全局函数的地址 * | sort > up.txt 通过管道将 nm up 的输出作为 sort 的输入, sort 排序后输出重定向到 up.txt 文件中(输出有1910行, 不得不这么做o(╯□╰)o) `  `然后发现两个地址分别位于 `__libc_start_main`、_start 中: ... 08048140 T _init 080481c0 T _start 080481f0 t __do_global_dtors_aux 08048260 t frame_dummy 080482bc T main 08048300 T __libc_start_main 080484d0 T __libc_check_standard_fds ... `  `实际上程序正好是从 _start 开始执行的, 而且从 up 的反汇编结果中可看出 _start 的第一条指令 xor %ebp,%ebp 就是那条传说中的将 ebp 清零的指令 (两个一样的数相异或的结果一定是0)。   那么调用 main 函数之前程序都干了些啥事呢? 比如说堆的初始化,如果是 C++ 程序的话, 全局对象的构造也是在 main 之前完成的 (不能让 main 中使用全局对象的时候竟然还没构造吧!), 而全局对象的析构也相当有趣地在 main 执行完了之后才执行。   main 在你心目中的地位是不是一落千丈了? [回目录][content] ================================================ FILE: mem.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

进程内存分布

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

变量名、函数名

  C程序在执行的时候直接用内存地址去定位变量、函数, 而不是根据名字去搜索,所以C程序执行的速度比脚本语言要快不少。   对于函数中的局部变量来说,编译为汇编的时候, 名字就已经被彻彻底底地忘记了, 因为局部变量在函数帧中,这一帧要占多少字节, 各局部变量在帧中的相对位置, 都在编译成汇编的时候就可以确定下来, 生成目标文件、可执行文件的时候也不需要再更改。   而 全局变量、static变量、函数 由于要将所有目标文件、 库链接到一起之后才能最终确定它们的绝对地址, 所以在链接前名字还是标志着它们的存在。 它们的信息存储在符号表(符号数组)中, 其中每一项除了有符号名,还有符号地址(链接后填入), 所以 nm 命令可得到 地址-符号名 映射。 虽然程序运行时用不到符号表, 但是默认情况下可执行文件中还是存着符号表, 看下面这个程序(name.c): #include int globalvar; int main() { static int staticval; return 0; } `  `name.c 中有全局变量、static变量、函数(main), 查看它编译后的目标文件、可执行文件的 地址-符号 映射: [lqy@localhost notlong]$ gcc -c name.c [lqy@localhost notlong]$ nm name.o 00000004 C globalvar 00000000 T main 00000000 b staticval.1672 [lqy@localhost notlong]$ gcc -o name name.c [lqy@localhost notlong]$ nm name | sort 08048274 T _init 080482e0 T _start 08048310 t __do_global_dtors_aux 08048370 t frame_dummy 08048394 T main ... 此处省略X行 ... 08049604 b staticval.1672 08049608 B globalvar 0804960c A _end U __libc_start_main@@GLIBC_2.0 w __gmon_start__ w _Jv_RegisterClasses [lqy@localhost notlong]$ `  `可执行文件中的 地址-符号 映射还有什么存在的意义呢? 它可用于汇编级调试的时候设置断点, 比如linux内核编译后就生成了 System.map 文件, 便于进行内核调试: 00000000 A VDSO32_PRELINK 00000040 A VDSO32_vsyscall_eh_frame_size 000001d3 A kexec_control_code_size 00000400 A VDSO32_sigreturn 0000040c A VDSO32_rt_sigreturn 00000414 A VDSO32_vsyscall 00000424 A VDSO32_SYSENTER_RETURN 01000000 A phys_startup_32 c1000000 T _text c1000000 T startup_32 c1000054 t default_entry c1001000 T wakeup_pmode_return c100104c t bogus_magic c100104e t save_registers c100109d t restore_registers c10010c0 T do_suspend_lowlevel c10010d6 t ret_point c10010e8 T _stext c10010e8 t cpumask_weight c10010f9 t run_init_process c1001112 t init_post c10011b0 T do_one_initcall ... [回目录][content] ================================================ FILE: optimize.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

编译优化

  C语言没有汇编快,因为C语言要由编译器翻译为汇编, 编译器毕竟是人造的,翻译出来的汇编源代码总有那么N条 指令在更智能、更有创造性的我们看来是多余的。   C语言翻译后的汇编有如下恶劣行径: 1. C语言偏爱内存。我们写的汇编一般偏爱寄存器, 寄存器比内存要快很多倍。当然,寄存器的数量屈指可数, 数据多了的话也必须用内存。 2. 内存多余读。假如在一个 for 循环中经常要执行 ++i 操作,编译后的汇编可能是这样的情形: movl i, %eax addl $1, %eax movl %eax, i 即使 eax 寄存器一直存着 i 的值, C语言也喜欢操作它前先读一下,以上3条指令浓缩为一条 incl %eax 速度就快上好几倍了。 `  `尽管C语言"如此不堪",但是考虑到高级语言带来的 源码可读性和开发效率在数量级上的提高,我们还是原谅了它。 而且很多编译器都有提供优化的选项, 开启优化选项后C语言翻译出来的汇编代码几近无可挑剔。   VC、VS有 Debug、Release 编译模式, Release 下编译后,程序的大小、执行效率都有显著的改善。 gcc 也有优化选项,我们来看看 gcc 优化的神奇效果:   我故意写了一个垃圾程序(math.c): #include int main() { int a=1, b=2; int c; c = a + a*b + b; printf("%d\n", c); return 0; } 且看看不优化的情况下,汇编代码有多么糟糕: 编译命令:gcc -S math.c main部分的汇编代码: main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $32, %esp movl $1, 28(%esp) # 28(%esp) 是 a movl $2, 24(%esp) # 24(%esp) 是 b movl 24(%esp), %eax #\ addl $1, %eax #-\ imull 28(%esp), %eax #-eax=(b+1)*a addl 24(%esp), %eax #\ movl %eax, 20(%esp) #-c=(b+1)*a+b movl $.LC0, %eax movl 20(%esp), %edx movl %edx, 4(%esp) movl %eax, (%esp) call printf movl $0, %eax leave ret 汇编代码规模庞大,翻译水平中规中矩。 现在开启优化选项: 编译命令:gcc -O2 -S math.c main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $16, %esp movl $5, 4(%esp) movl $.LC0, (%esp) call printf xorl %eax, %eax leave ret `  `规模变为原来的一半,而且 gcc 发现了 a、b、c 变量是多余的,直接将结果 5 传给 printf 打印了出来 ——计算器是编译器必备的一大技能。 初中那时候苦逼地做计算题,怎么就不学学C语言呢O(∩_∩)O~ [回目录][content] ================================================ FILE: pfunc.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

函数指针

## 一、函数指针的值   函数指针跟普通指针一样,存的也是一个内存地址, 只是这个地址是一个函数的起始地址, 下面这个程序打印出一个函数指针的值(func1.c): #include typedef int (*Func)(int); int Double(int a) { return (a + a); } int main() { Func p = Double; printf("%p\n", p); return 0; } `  `编译、运行程序: [lqy@localhost notlong]$ gcc -O2 -o func1 func1.c [lqy@localhost notlong]$ ./func1 0x80483d0 [lqy@localhost notlong]$ `  `然后我们用 nm 工具查看一下 Double 的地址, 看是不是正好是 0x80483d0: [lqy@localhost notlong]$ nm func1 | sort 08048294 T _init 08048310 T _start 08048340 t __do_global_dtors_aux 080483a0 t frame_dummy 080483d0 T Double 080483e0 T main ... `  `不出意料,Double 的起始地址果然是 0x080483d0。 ## 二、调用函数指针指向的函数   直接调用一个函数是 call 一个常量, 而通过函数指针调用一个函数显然不能这么做, 因为函数地址是可变的了,指向谁就得 call 谁。 下面比较一下直接调用和通过函数指针间接调用同一个函数的 汇编代码(func2.c): #include typedef int (*Func)(int); int Double(int a) { return (a + a); } int main() { Func p = Double; Double(2); // 直接调用 p(2); // 间接调用 return 0; } `  `部分汇编代码如下: movl $2, (%esp) call Double movl $2, (%esp) movl 28(%esp), %eax # 28(%esp) 是 p call *%eax `  `可见通过函数指针间接调用一个函数, call 指令的操作数不再是一个常量, 而是寄存器 eax(其它寄存器应该也行), 此时 eax 寄存器的值正好是 Double 函数的起始地址, 所以接着就会去执行 Double 函数的指令。 ## 三、参数弱匹配   从上面的例子中我们也看到了函数指针也没什么特别的, 也就存了个地址,但是调用一个函数不仅需要知道它的起始地址, 还得根据它的参数列表来压栈传递参数。   参数列表在定义函数指针类型的时候就约定好了, 凡是具有相同参数列表的函数都可以赋值给该类型的函数指针, 而参数列表不同的函数也可以通过强制类型转换后赋值给它 (C语言的指针类型可以任意转换⊙﹏⊙), 下面这个程序就大胆的强制转换了一下(func3.c): #include typedef int (*Func)(int); int Double2(int a, int b) { return (a + a); } int main() { Func p = (Func)Double2; printf("%d\n", p(2)); return 0; } `  `不强制转换的话,编译的时候会报告一个 warring (居然不是 error ⊙﹏⊙), 上面这个程序编译的时候 0 error 0 warring, 执行也没有出错: [lqy@localhost notlong]$ gcc -o func3 func3.c [lqy@localhost notlong]$ ./func3 4 [lqy@localhost notlong]$ `  `真算是朵奇葩了!   没有出错的原因是:参数 a 对应的刚好是压栈的 2, 而 b 对应的是一个危险地带,还好没用到 b, 所以这个程序依然顺利地执行完了。   综上所述,函数指针真没什么特别的。 [回目录][content] ================================================ FILE: process0.01.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [Ŀ¼][content]

linux0.01ʱƬĺ

ǡlinuxϵеĵһƪΪʲôҪӽ̿ʼأ Ϊϵͳš͵ľǽ̣ Խ̵ĹҪIJ־ǶʱƬĴ ⲿֵ㷨Ӱ쵽лٶȣ DzϵͳһҪָ꣩ ͬʱֳϵͳ ǡϵͳἰ --- ȣвϵͳӦеԣ 1. ÿ̶ᱻɸʱƬʱƬ꣬ ʱͲͶ 2. ÿ̶ȼȼߵĽ̷϶ʱƬ 3. ʱƬ󣬽ûսᣬӦٷʱƬ ܲһ̱ʱɰ^_^ ``linux0.01һʱƬ100.01룩 ̵ȼ1~15ȼ· ʱƬʱ̻õʱƬ ôʱƬôĵأ ﲻòǵĴ󹦳8253 û΢ԭϽĿɱ̶ʱ/ ÿ10msһʱжϣ жϳаѵǰ̵ʱƬ1 ʱƬĵġ ˵жϣһҲϤģǾ8259жϿ оƬͲ˵ˣ߾Ϳʼᷳƪ O(_)O~ --- ǰ̵ʱƬ0֮ͻschedule лһеʣʱƬḶ̌ Ҫн̣ǸO(n)ʱ临ӶȵĹ n̵ĸ *ע⣺һ£ǰֻԼеʱƬ֮ scheduleÿһʱƬ͵һschedule ʹˡ* ģһ½̵ĵ 1. 3ֱ̣ʣʱƬ543ȼ15 ʱ schedule ô1ᱻѡΪҪִеḶ̌ ![1](http://fmn.rrimg.com/fmn063/20121022/1950/original_9rlY_112f0000631a1191.jpg) 2. ִеһ50msĹ1ʱƬ ![2](http://fmn.rrimg.com/fmn063/20121022/1950/original_zHfv_346c000051721190.jpg) 3. ʱʱжϳлscheduleл2ִУ ǽ2ִ20msڵȴIOˣ ![3](http://fmn.rrimg.com/fmn060/20121022/1950/original_eMlh_2ddb000063e4118f.jpg) 4. ߵĺлscheduleл3ִУ һֱ3ʱƬҲȫ꣺ ![4](http://fmn.rrimg.com/fmn065/20121022/1950/original_jC84_7acf000062c5118e.jpg) 5. ڽ1ͽ3ûʱƬ˲ͶУ 2˲ͶУ scheduleϣͨʱƬΣ ÿµʱƬļ㹫ʽǣ µʱƬ = ɵʱƬ / 2 + ȼ ʽӿе֣ɵʱƬӦö0 ģпԿߵĽܻ̿ʱƬ ߵĽ̾ͱرˣ ![5](http://fmn.rrimg.com/fmn063/20121022/1950/original_Gfvs_7179000063f0118d.jpg) ߵḺ̌رյԭIOƵ ı༭׽״̬ رյĻѱscheduleѡУ ʱƬʱ2ԭ2ʱƬ䣬 ʱƬպñѣʱ1ͽ3ʱƬ15 ࣬Ҳ˵ҪȽ1ͽ3̱֮ 2ܱͶС⻹ֻ2CPUḶ̌ 300ms2ˣĽ20أ Ǿ͵3sǴֵλҪðˣ Ĺ̻һʱϣģ ![timeline](http://fmn.rrimg.com/fmn063/20121022/1950/original_lAYT_11df0000635e118c.jpg) --- ## ܽ ``linux0.01л(schedule)ĿУ 1. ѡʣʱƬĿеĽ(O(n)) Ул 2. ûпͶеḶ̌·ʱƬ(O(n)) ִ1 ``linux0.01ĽлO(n)Ӷȵģ O(nlog(n)) O(n) 漣ǽлĿܴ O(n) O(1) ֪Σ»طֽ⡣ [Ŀ¼][content] ================================================ FILE: process2.6.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [Ŀ¼][content]

linux2.6.XXлʱƬ

һƪнlinux0.01Ľлʱ临ӶO(n)ģ linux0.01˵ʲô⣬ Ϊlinux0.01ֻ64̡ ڵlinuxϵͳмǧִ̲У Ϊ˸ܵصȽ̣ʱƬ㷨ҲԽԽӣ ȻO(n)л㷨ǾеΣˡ һƪпԿʱҪط 1. ѡʵĽ̽лO(n) 2. ʱƬO(n) ``linux2.6.XXгɹİǽO(1) --- # ѡ̽л linux2.6.XX иװ˽ϢĽṹ壺 ȼ(struct prio_array)ṹ3Ա 1. ̸ 2. ȼλͼ 3. ȼе ``λͼÿһλӦһȼУ зǿգǶӦһλ1 ![bits](http://fmn.rrimg.com/fmn062/20120928/2320/original_boj5_0d35000017351190.jpg) linux2.6.XX Ľ140ȼ ԶӦλͼӦ140أʵ5unsigned long ɣ160ءlinux0.01ͬ2.6ȼ ֵԽСȼԽߡ2.6ִʣʱƬḶ̌ ִȼߵĽ̡ ԵȵʱֻҪҳ1Ϊ1λ ȻӸλӦĶгһ̾Ϳˡ ѰҵһΪ1λbsflָ һο4ֽڣ۴ڶٽָ౻ִ5Σ 㿪˵΢ ǰλͼôڿеĽ ȼ7ֻҪ7гһ̣лˡ --- # ʱƬ ǰ̵ʱƬĹ֮·ʱƬ ʱƬúٷõԭȼ ȼĽ̾͵õȸȼĽ˲ͶУ ͲǶˣ ʵһȼ飨Ϊexpired ֮ǰ̸۵һֱactiveȼ飩 ٷʱƬĽ̶õexpired֮С һʱ activeеĽһתƵexpired֮У ʱscheduleύһactiveexpired 򵥵ؽһָͺˣ ȻֿԴactiveѡˡ ͼIJïɣ 1. ԭ3̣ABCǵȼֱ141616 ![1](http://fmn.rrimg.com/fmn057/20120928/2320/original_ZYS3_1591000055d5125e.jpg) 2. ȼAᱻִУAʱƬˣ Aᱻ·ʱƬõexpiredУ ![2](http://fmn.rrfmn.com/fmn059/20120928/2320/original_UTKq_2df900008cff125d.jpg) 3. BCȼߵģB迿λͼǶͷ ʵ˫ҾͲˣBִ ![3](http://fmn.rrimg.com/fmn062/20120928/2320/original_AOhZ_310800008d9c125c.jpg) 4. C ![4](http://fmn.rrfmn.com/fmn058/20120928/2315/original_V9Ta_1eec00008e33125b.jpg) 5. activeѾûнˣָ룺 ![5](http://fmn.rrimg.com/fmn056/20120928/2320/original_cNKM_5ebe00008c481191.jpg) ``ÿһôļ򵥣 ̵лʱ俪Ѿ̵û̫Ĺϵˡ --- ʵʱƬٷO(1)Ǹ ΪnٷʱƬܿO(n)ġ ʵǽʱƬĹ̯ÿˣ ÿ̶мʮmsʱȥУ ҲںѼusʱԼһʱƬ linux0.01ƾͲˣ ٸ̶ﵽһл̵ļȡʱƬ леٶˡ ѡбO(1)һ׳ˣ һƪᵽO(nlog(n))O(n) ԭһģ * * 鲢򡢿() -> * O(nlog(n)) -> O(n) * Ƚ -> DZȽ * л * 0.01 -> 2.6.XX * O(n) -> O(1) * Ƚ -> DZȽ Ǵnѡһ̣ 0.01ʹñȽʣʱƬҳģ 2.6.XXȻȼλͼҳһΪ1λ ûбȽϡ [Ŀ¼][content] ================================================ FILE: recur.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

所有递归都可以变循环

  这是函数帧的应用之二。   还记得大一的C程序设计课上讲到汉诺塔的时候老师说: 所有递归都可以用循环实现。这听起来好像可行, 然后我就开始想怎么用循环来解决汉诺塔问题, 我大概想了一个星期,最后终于选择了……放弃…… 当然,我不是来推翻标题的, 随着学习的深入,以及"自觉修炼",现在我可以肯定地告诉大家: 所有递归都可以用循环实现,更确切地说: 所有递归都可以用循环+栈实现 (就多个数据结构,还不算违规吧O(∩_∩)O~)。   通过在我们自定义的栈中自建函数帧, 我们可以达到和函数调用一样的效果。 但是因为这样做还是比较麻烦,所以就不转换汉诺塔问题了, 而是转换之前的那个递归求解阶乘的程序(fac.c): #include int fac(int n) { if(n <= 1) return 1; return n * fac(n-1); } int main() { int n = 3; int ans = fac(n); printf("%d! = %d\n", n, ans); return 0; } ## 技术难点   我们可以在自建的函数帧中存储局部变量、存储参数, 但是我们不能存返回地址,因为我们得不到机器指令的地址! 不过,C语言有一个类似于指令地址的东西:switch case 中的 case子句,我们可以用一个case代表一个地址, 技术难点就此突破了。 ## 源程序   虽然我简化了很多步骤,但源程序还是比较长(fac2.c): #include // 栈的设置 #define STACKDEEPTH 1024 int stack[STACKDEEPTH]; int *esp = &stack[STACKDEEPTH]; #define PUSH(a) *(--esp) = a #define POP(b) b = *(esp++) // 其它模拟寄存器 int eax;// 存返回值 int eip;// 用于分支选择 int main() { int n = 3; // 模仿 main 调用 fac(n) PUSH(n); PUSH(10002);// 模仿返回 main 的地址 eip = 10000; do{ switch(eip){ case 10000: --esp;// 为帧分配空间 if(esp[2] <= 1){// 模仿递归终止条件 eax = 1; ++esp;// 回收帧空间 POP(eip); }else{// 模仿递归计算 fac(n-1) esp[0] = esp[2] - 1; PUSH(10001); eip = 10000; } break; case 10001:// 返回 n * (fac(n-1)的结果) eax = esp[2] * eax; ++esp;// 回收帧空间 POP(eip); break; } }while(eip != 10002); printf("%d! = %d\n", n, eax); return 0; } ## 自建的函数帧   为了简化程序,ebp我们就不用了, 完全用esp来操作栈,一个函数帧只占用 8 个字节: ![recur1](http://fmn.rrimg.com/fmn063/20121130/1830/original_OJ3e_0ad000003200125b.jpg)   在计算到 fac(1) 的时候,栈中内容如下: ![recur2](http://fmn.rrimg.com/fmn056/20121130/1830/original_LBeS_30f500003160118f.jpg)   比起肆意挥霍栈空间的 gcc(fac帧用了32字节, 浪费了20字节,实际使用了12字节), 我们的程序真的是太节省了(一帧只用8字节)。 ## 小结   当然,本文的方法只用于学术讨论, 说明所有递归都可以变循环,编程的时候还是不要这么用。 因为代码复杂、容易出错、难以理解, 唯一的优点是能省空间省到极限。   这种递归变循环的方式并没有降低时间复杂度, 但却是通用的(所有递归都可以这么变循环); 而有一部分递归可以基于巧妙的算法变成循环, 并且大大降低时间复杂度,如:动态规划、贪心算法 (详见《算法导论》)。 [回目录][content] ================================================ FILE: static.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

static变量 及 作用域控制

## 一、static变量   static变量放在函数中,就只有这个函数能访问它; 放在函数外就只有这个文件能访问它。 下面我们看看两个函数中重名的static变量是怎么区别开来的 (static.c): #include void func1() { static int n = 1; n++; } void func2() { static int n = 2; n++; } int main() { return 0; } `  `下面是编译后的部分汇编: func1: pushl %ebp movl %esp, %ebp movl n.1671, %eax addl $1, %eax movl %eax, n.1671 popl %ebp ret func2: pushl %ebp movl %esp, %ebp movl n.1674, %eax addl $1, %eax movl %eax, n.1674 popl %ebp ret `  `好家伙!编译器居然"偷偷"地改了变量名, 这样两个static变量就容易区分了。   其实static变量跟全局变量一样被放置在 .data段 或 .bss段 中,所以它们也是程序运行期间一直存在的, 最终也是通过绝对地址来访问。 但是它们的作用域还是比全局变量低了一级: static变量被标识为LOCAL符号,全局变量被标识为GLOBAL符号, 在链接过程中,目标文件寻找外部变量时只在GLOBAL符号中找, 所以static变量别的源文件是"看不见"的。 ## 二、作用域控制   作用域控制为的是提高源代码的可读性, 一个变量的作用域越小,它可能出没的范围就越小。   C语言中的变量按作用域从大到小可分为四种: 全局变量、函数外static变量、函数内static变量、局部变量: 1. 全局变量是杀伤半径最大的:不仅在定义该变量的源文件中可用, 而且在任一别的源文件中只要用 extern 声明它后也可以使用, 因此,当你看到一个全局变量的时候应该心生敬畏! 2. 函数外的static变量处于文件域中, 只有定义它的源文件中可以使用。如果你看到一个static变量, 那是作者在安慰你:哥们(妹子),这个变量不会在别的文件中出现。 3. 函数内static变量在函数的每次调用中可用(只初始化一次), 它同以上两种变量一样在程序运行期间一直存在, 所以它的功能是局部变量无法实现的。 4. 局部变量在函数的一次调用中使用, 调用结束后就消失了。 `  `显然,作用域越小越省心, 该是局部变量的就不要定义成全局变量, 如果"全局变量"只在本源文件中使用那就加个static。   即便是局部变量也还可以压缩其作用域:   有的同学写的函数一开头就声明了函数中要用到的所有局部变量, 一开始我也这么做,因为我担心:如果把变量定义在循环体内, 是不是每一次循环都会给它们分配空间、回收空间,从而降低效率? 但事实是它们的空间在函数的开头就一次性分配好了(scope.c): #include int main() { int a = 1; { int a = 2; { int a = 3; } { int a = 4; } } return 0; } 编译后的汇编代码如下: main: pushl %ebp movl %esp, %ebp subl $16, %esp movl $1, -4(%ebp) movl $2, -8(%ebp) movl $3, -12(%ebp) movl $4, -16(%ebp) movl $0, %eax leave ret `  `各层局部环境中的变量a是subl $16, %esp一次性分配好的。 由此可见不是每个{}都要分配回收局部变量, 一个函数只分配回收一次。因此, 如果某个变量只在某个条件、循环中用到的话, 还是在条件、循环中定义吧,这样, 规模比较大的函数的可读性将提高不少,而效率丝毫没有下降, 可谓是百利而无一害! [回目录][content] ================================================ FILE: staticstack.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

C语言的栈是静态的

  C语言有了可变参数之后,我们可以传任意个数的参数了, 似乎挺动态的了,但是可变参数函数还是不够动态。 ## 一、鞭长莫及   我们可以在 main 中写出好几条参数个数不同的调用 sum 的语句, 但是具体到某一条语句,sum 的参数个数是一定的, 比如上一篇中的 sum(2, 3, 4) 的参数个数是 3。 如果程序运行中调用 sum 函数的时候, 参数个数根据用户输入而定,那就不能用可变参数来实现了。 也就是说不能用 sum 来实现以下这个函数的功能: // 将数组 a 的所有元素(个数为 n)求和后返回 int d_sum(int n, int a[]); `  `当然,这个函数不用 sum 来做是很好实现的。 我再换一个问题,下面这个函数怎么用 printf 来实现: // fmt 存的是格式串,它描述了 n 个整数(数组 a 中) // 的格式,某次调用如下: // int a[] = {1, 2, 3}; // d_printf("%d+%d=%d", 3, a); void d_printf(const char *fmt, int n, int a[]); `  `这就没法做了吧! ## 二、寻根究底   d_printf 没法实现的原因是这样的代码真没法写: 传给 printf 的参数的个数到运行的时候才知道, 而调用 printf 的语句又必须明确的列出所有参数。   其根本原因是C语言的栈是静态的, 上一篇的 va.c 编译后的汇编代码如下: main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $16, %esp # 给main帧分配栈空间 movl $4, 8(%esp) movl $3, 4(%esp) movl $2, (%esp) call sum # 调用变参函数 sum movl $.LC0, (%esp) movl %eax, 4(%esp) call printf # 调用变参函数 printf xorl %eax, %eax leave ret `  `可以看到虽然 main 函数中调用了两个变参函数, 但是栈却没有一点动态可变的意思,居然是用一条 subl $16, %esp 分配了固定的 16 字节的栈空间 (编译的时候计算得出需要12字节,取整吧,16字节!)。   而在 d_printf 的实现中需要分配 4+4*n 字节的栈空间, 用于存传给 printf 的 格式串指针 和 n个整数, 用C语言是没法实现啰。 ## 三、另辟蹊径   C语言不能直接使用寄存器,但是汇编可以, 如果我们在 d_printf 中嵌入一段汇编来修改 esp 寄存器, 达到动态分配栈空间的效果,然后存入参数,call printf, 就可以完成任务了。   接下来的两篇就来实现 d_printf 啰! [回目录][content] ================================================ FILE: string.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [Ŀ¼][content]

ַ

һƪַַʹã Ҳ٣ ## һַĴ洢λ CԴstring1.c #include int main() { puts("Hello, World!"); return 0; } ``ֱӿִļķ [lqy@localhost temp]$ gcc -o string1 string1.c [lqy@localhost temp]$ ./string1 Hello, World! [lqy@localhost temp]$ objdump -s -d string1 > string1.txt [lqy@localhost temp]$ ``string1.txt еIJ£
Contents of section .rodata:
 8048488 03000000 01000200 00000000 48656c6c  ............Hell
 8048498 6f2c2057 6f726c64 2100               o, World!.      

...

080483b4 <main>:
 80483b4:	55                   	push   %ebp
 80483b5:	89 e5                	mov    %esp,%ebp
 80483b7:	83 e4 f0             	and    $0xfffffff0,%esp
 80483ba:	83 ec 10             	sub    $0x10,%esp
 80483bd:	c7 04 24 94 84 04 08 	movl   $0x8048494,(%esp)
 80483c4:	e8 27 ff ff ff       	call   80482f0 <puts@plt>
 80483c9:	b8 00 00 00 00       	mov    $0x0,%eax
 80483ce:	c9                   	leave  
 80483cf:	c3                   	ret    
ɼ 0x8048494 Ӧ "Hello, World!" ĵַ Ȼ .rodata 0x8048488 + 12 ô洢 "Hello, World!" ĸַ ASCII 'H' ASCII 0x48 ̾ '!' ASCII 0x21 ԶΪ˸ַ 0x00 ֻҪ˫ַԶΪǼӽ ɴǷ֣ ַȫֱһΪ̬ݴ洢ڿִļУ ʹõʱóַʡ ַ .rodata У е .text ΣΣеһ ڿִļڴʱ ֻģֻͨҳʵֵģ ҳһλΪ 1 ʾҳд Ϊ 0 ʾҳֻͼֻҳдݣ CPU ͻᴥҳ쳣 Դַеκַڳʱ ## ַָ ַ CԴstring2.c #include int main() { char *s1 = "1234567"; char s2[]= "1234567"; puts(s1); puts(s2); return 0; } ``ִļķĸֵ£ 80483bd: c7 44 24 1c c4 84 04 movl $0x80484c4,0x1c(%esp) 80483c4: 08 80483c5: a1 c4 84 04 08 mov 0x80484c4,%eax 80483ca: 8b 15 c8 84 04 08 mov 0x80484c8,%edx 80483d0: 89 44 24 14 mov %eax,0x14(%esp) 80483d4: 89 54 24 18 mov %edx,0x18(%esp) ``0x80484c4 ַ"1234567"ĵַ ԣֵַָʱݵԴַĵַ ֲֵַʱҪһֲռ ˵2ַʽ˷ѿռ䣨4ֽ vs 8ֽڣ ˷ʱ䣨1mov vs 4movǵ2ַʽҲ һǴ1ַʽݵԴַĵַ ԴַֻҳУ޷޸ģַȴ޸ 飺бûĶַ Ǿָɣ ַ ̬Ŀռ 档 ## ʽ ת ַ֣иʽתȥ CԴstring3.c #include int main() { printf("--------\n%d\n", 123); return 0; } ### Դļ gcc -S string3.c ``£ .LC0: .string "--------\n%d\n" ### Ŀļ gcc -c string3.c objdump -s -d string3.o > string3.txt ``-c Ĭ string3.o ļУ string3.txt еַ Contents of section .rodata: 0000 2d2d2d2d 2d2d2d2d 0a25640a 00 --------.%d.. ``'\n'滻 0x0a%d û ### ִļ gcc -o string3 string3.c objdump -s -d string3 > string3.txt ``ִļĿļһ Contents of section .rodata: 80484a8 03000000 01000200 00000000 2d2d2d2d ............---- 80484b8 2d2d2d2d 0a25640a 00 ----.%d.. ``֪ˣתڱɶļ ͱתΪǸַ ʽȻҪ printf ʱõġ ֪ʲôã printf ôתַǾͲòˣ תȥġ 뿴 printf ŵʵ֣ linux 0.01 kernel/vsprintf.c һ򻯵 ûʵָĴʵ֣תַDzĵġ linux 汾ںԴ룺http://www.kernel.org/pub/linux/kernel/ linux 0.01http://www.kernel.org/pub/linux/kernel/Historic/ [Ŀ¼][content] ================================================ FILE: struct.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

结构体

  结构体是 C 语言主要的自定义类型方案, 这篇就来认识一下结构体。 ## 一、结构体的形态   C源程序(struct.c): #include typedef struct{ unsigned short int a; unsigned short int b; }Data; int main() { Data c, d; c.a = 1; c.b = 2; d = c; printf("d.a:%d\nd.b:%d\n", d.a, d.b); return 0; } `  `赋值部分翻译后: movw $1, 28(%esp) # c.a = 1 movw $2, 30(%esp) # c.b = 2 movl 28(%esp), %eax # movl %eax, 24(%esp) # d = c `  `可以看出: * c.a 是在 28(%esp) 之后的2个字节 * c.b 是在 30(%esp) 之后的2个字节 * c 是 28(%esp) 之后的4个字节 * d 是 24(%esp) 之后的4个字节 `  `不得不感叹名字(结构体名字、子元素名字)再一次被抛弃了, 子元素名代表的是相对于结构体的偏移。 ## 二、结构体的复制   大一的时候,老师千叮咛万嘱咐:数组不能复制!, 但是当发现下面这个程序正常运行后,我困惑了(block.c): #include typedef struct{ char data[1000]; }Block; Block a={{'a','b','c',}}; int main() { Block b; b=a; puts(b.data); return 0; } `  `Block a={{'a','b','c',}} 是对 a 的部分初始化, 'c' 后面自动填 0,写成 Block a={{"abc"}} 也一样, C 语言对初始化还是很宽容的。   上面这个程序居然正常的编译、运行了,这究竟是怎样的逆天? 看看汇编部分: leal 24(%esp), %edx movl $a, %ebx movl $250, %eax movl %edx, %edi # edi = &b movl %ebx, %esi # esi = &a movl %eax, %ecx # ecx = 250 rep movsl `  `我们发现程序确实通过 250 次 movsl 复制了一个"数组"。 其原因是:结构体是可以复制的, 结构体又可以包括任意类型的子元素,数组也行, 所以"数组"也被复制了。   那为什么纯粹的数组就不能复制呢? 我们可以这样去理解:一个变量能被复制的必要条件是 我们知道它的大小。结构体做为自定义类型, 在编译的时候编译器必然存储了它的子元素类型、个数等相关信息, 结构体的大小也就知道了;而数组一般只在乎它的类型和 起始地址,元素个数总是被忽视的(例如: void func(char s[]) 可接受任何长度的字符数组做参数), 而且元素个数也没有被当做数组的一部分存入内存, 所以数组的复制是不好实现的。 ## 小结   如果给结构体下一个实在点的定义话,那就是: 有格式的字节数组。有了结构体后 C 语言的 变量类型就丰富多了,但是同时也要注意: 1. 超过 4 字节的结构体不宜做参数(参数传递浪费时间、空间), 换做指针更好。 2. 超过 4 字节的结构体不宜做返回值类型 (话说一般返回值都用 eax 来存, 那么超过 4 字节的时候怎么存呢?自己去探索吧!)。 [回目录][content] ================================================ FILE: varargs.md ================================================ [content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content [回目录][content]

可变参数

   C语言的可变参数的实现非常巧妙: 大师只用了 3 个宏就解决了这个难题。 ## 一、可变参数的应用   这里实现一个简单的可变参数函数 sum: 它将个数不定的多个整型参数求和后返回, 其第 1 个参数指明了要相加的数的个数(va.c): #include #include // 要相加的整数的个数为 n int sum(int n, ...) { va_list ap; va_start(ap, n); int ans = 0; while(n--) ans += va_arg(ap, int); va_end(ap); return ans; } int main() { int ans = sum(2, 3, 4); printf("%d\n", ans); return 0; } `  `sum 函数的第一个参数是 int n, 逗号后面是连续的 3 个英文句点, 表示参数 n 之后可以跟 0、1、2…… 个任意类型的参数。 sum 可以这么用: sum(0); sum(1, 2); sum(3, 1, 1, 1); ## 二、可变参数的实现   可以看到在 sum 函数中用到了 3 个函数一样的东西: va\_start、va\_arg、va\_end, 它们是标准库(意味着各种平台都有)头文件 stdarg.h 中 定义的宏,这 3 个宏经过清理后是下面这个样子: typedef char* va_list; #define va_start(ap,v) ( ap = (va_list)(&v) + sizeof(v) ) #define va_arg(ap,t) ( *(t *)((ap += sizeof(t)) - sizeof(t)) ) #define va_end(ap) ( ap = NULL ) * va\_start 将 ap 定位到可变参数列表的起始地址 * va\_arg 每次返回一个参数,并后移 ap 指针 * va\_end 将 ap 置 NULL(避免非法使用) `  `这 3 个宏的实现就是基于 C语言默认调用惯例是从右至左 将参数压栈的事实,比如说 va.c 中调用 sum 函数, 参数压栈的顺序为:4->3->2, 又因为 x86 CPU 的栈是向低地址增长的, 所以参数的排列顺序如下: ![args](http://fmn.rrimg.com/fmn062/20121221/1930/original_qweH_1b90000008bb125c.jpg)   va\_start(n, ap) 就是 ( ap = (char*)(&n) + 4 ) 因此 ap 被赋值为 ebp+12 也就是变参列表的起始地址。   之后 va\_arg 取出每一个参数: ( *(int *)((ap += 4) - 4) ) 它首先将变参指针 ap 右移到下一个参数的起始地址, 再将加赋操作的返回值减到之前的位置取出一个参数。 这样,用一条语句既取出了当前参数,又后移了指针 ap, 真是神了!   sum 中循环使用 va\_arg 就取出了 n 个要相加的整数。 ## 三、变参函数的可行性   一个变参函数能接受个数、类型可变的参数, 需要满足以下两个条件: 1. 能定位到可变参数列表的起始地址 2. 能获知可变参数的个数、每个参数的大小(类型) `  `条件 1 只要有个前置参数就能满足, 而对于这样的变参函数:void func(...); 编译能通过,但是不能用 va_start 取到变参列表的起始地址, 所以基本不可行。
  sum 函数中参数 n 被用来定位可变参数列表的起始地址 (满足条件1);n 的值是可变参数的个数, 类型默认全部是 int 型(满足条件2), 因此 sum 能正常工作。
  再看看 printf 函数是如何满足以上两个条件的, printf 函数的原型是: int printf(const char *fmt, ...); `  `printf 的第1个参数 fmt(格式串)被用来定位其后 的可变参数的起始地址(满足条件1); fmt 指向的字符串中的各个格式描述符如:%d、%lf、%s 等 告诉了 printf fmt 之后参数的个数、各个参数的类型 (满足条件2),因此 printf 能正常工作。
  当然,sum、printf 能正常工作是设计者一厢情愿的期望, 如果使用者不按规矩传入参数、格式串,函数能正常工作才怪! 比如: sum(2, "111", "222"); printf("%s", 0); `  `编译器可不会进行可变参数的类型检查、格式串-参数匹配, 后果将会在运行的时候出现…… [回目录][content]