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
================================================
<a name="top"></a>
<h1 align="center"><b>朴素linux</b></h1>
大学里我坚持的最久的一项任务就是自学 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 语言方面的底层知识。那些既想了解底层
又不愿意系统地看源代码或操作系统方面书籍的同学可以来看看,
就当是看一部小说吧。
这个系列的名字叫<b>朴素linux</b>。
以下是目录,目录随着进度变更,
还有可能被重新分类整理,请见谅。
<a name="content"></a>
* 解剖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]
<a name="top"></a>
<h1 align="center">内存对齐
</h1>
## 为什么要进行内存对齐
在计算机组成原理中我们学到:
一块内存芯片一般只提供 8 位数据线,要进行 16 位数据的读写
可采用<a target="_blank" href="http://baike.baidu.com/view/1881700.htm">奇偶分体</a>来组织管理多个芯片,
32 位也类似:

这样,连续的四个字节会分布在不同的芯片上,
送入地址 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 <stdio.h>
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 <stdio.h>
#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 <stdio.h>
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]
<a name="top"></a>
<h1 align="center">ָ
</h1>
ָʲô
Carray.c
#include <stdio.h>
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;
}
``༰עͣ
<table>
<tr><td>
<pre><code> .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) #/
</code></pre></td>
<td valign="bottom"><img src="http://fmn.rrfmn.com/fmn059/20121113/1850/original_fmSH_48030000e812125c.jpg" /></td>
</tr>
<tr><td>
<pre><code> 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
</code></pre></td>
<td valign="bottom"><img src="http://fmn.rrimg.com/fmn062/20121113/1850/original_6dpg_28e80000e7fc1191.jpg" /></td>
</tr>
<tr><td>
<pre><code> 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
</code></pre></td>
<td valign="bottom"><img src="http://fmn.rrimg.com/fmn056/20121113/1850/original_ECol_44540000c23f1190.jpg" /></td>
</tr>
<tr><td colspan="2">
<pre><code> 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
</code></pre></td>
</tr>
</table>
Ҷ lea ָܱȽİ mov ָ
lea ָݵڴĵַ mov ָݵĵ
ڴֵ磺
leal 24(%esp), %eax # 24+esp Ľ eax
movl 24(%esp), %eax # 2012 eax
## ܽ
ȣǿ main ҲһĻ
<em></em>з Double һĽṹ
Σָд洢ռģ32λ4ֽڵĴС
д洢һڴַֻеԪд洢ռ䣬
C оõַ棩
˵ûд洢ռġ轫ֵַһָ룬
ַĴʽǣ
* ȫ飬 mov ָеһ
* Ǿֲ飬 lea ָ esp ebp +
Ľ
``<b>ַһָIJ
ھֲռУҲȫֱռУ
DZ̻ڴ</b>
ˣַܱġ
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]
<a name="top"></a>
<h1 align="center">未初始化全局变量
</h1>
为下一篇介绍进程内存分布做准备,
这一篇先来介绍一下未初始化全局变量:
未初始化全局变量,这名字就很直白,就是 C 程序中定义成
全局作用域而又没有初始化的变量,我们知道这种变量在程序运行
后是被自动初始化为 全0 的。编译器编译的时候会将这类变量
收集起来集中放置到 .bss 段中,<b>这个段只记录了段长,
没有实际上的内容(全是0,没必要存储),
在程序被装载时操作系统会
为它分配等于段长的内存,并全部初始化为0</b>。
这有两个 C程序,都定义了全局数组 data(长度为1M,
占用内存4MB),一个部分初始化(bss\_init1.c),
一个未初始化(bss\_uninit1.c):
bss_init1.c:
#include <stdio.h>
#include <windows.h>
#define MAXLEN 1024*1024
int data[MAXLEN]={1,};
int main()
{
Sleep(-1);
return 0;
}
bss_uninit1.c:
#include <stdio.h>
#include <windows.h>
#define MAXLEN 1024*1024
int data[MAXLEN];
int main()
{
Sleep(-1);
return 0;
}
` `编译以上两个程序后:

可以看到有初始化的可执行文件的大小差不多是4MB,
而未初始化的只有47KB!这就是 .bss 段有段长,
而没有实际内容的表现。用 UltraEdit 打开 bss_init1.exe
可看到文件中大部分是全0(data数组的内容):

但是接下来运行(return 0 之前的 Sleep(-1) 保证了
程序暂时不会退出)的时候,却发现 bss_init1.exe
占用的空间明显少于 4MB,这是怎么回事呢?

这就涉及程序装载的策略了。早期的操作系统(如:linux 0.01)
采用的是一次装载:将可执行文件一次性完整装入内存后再执行程序。
不管程序是 1KB 还是 60MB,都要等全部装入内存后才能执行,
这显然是不太合理的。
而现在的操作系统都是采用延迟装载:
<b>将进程空间映射到可执行文件之后就开始执行了</b>,
执行的时候如果发现要读/写的页不在内存中,
就根据映射关系去读取进来,然后继续执行应用程序
(应该是在页保护异常的处理中实现的)。
bss_init1.exe 肯定是被映射了,<b>而程序中又没有对 data
数组进行读/写操作</b>,所以操作系统也就懒得去装入这片内存了。
下面修改一下这两个程序:在 Sleep(-1) 前将 data 数组
的每个元素赋值为 -1:
int i;
for(i=0; i<MAXLEN; ++i)
data[i] = -1;
` `再运行,它们占用的内存都是 4M 了:

[回目录][content]
================================================
FILE: byval.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[Ŀ¼][content]
<a name="top"></a>
<h1 align="center">ֵ
</h1>
ںһƪѾҿֵݵࣺ
<b>ʵΡβиԵĴ洢ռ䣨ʵҲֻһֵ
ûд洢ռ䣩ʵ -> βǸֵḶ́
ںǰ̣
˺жβνģʵεֵű䡣</b>
Ǿû취ںⲿˣ
ָͺˣ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. <b>Ϊ˽ʡռ</b>躯Ҫһⲿ
ռÿռܴĽṹ 4KB С
Ȼûָĵַ
ֻռ 4 ֽڣýṹΪ
Ҫռ 4KB ڴ棬һҪֵ
<b> char* ַҲԿdzڽʡռĿġ</b>
## 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);
``пԿ <b>2 ָҪָʱ</b>
һҪںΪָ붯̬ռ䡣
һ÷ָʵ֣жָҪһģ
2 ָҪܶ࣬Ϊֵֻ 1
ĸûƵġ
strdup صַҪ free Ŷ
⣬һûʹ 3 ָıҪ
[Ŀ¼][content]
================================================
FILE: call.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[Ŀ¼][content]
<a name="top"></a>
<h1 align="center">
</h1>
## ǰ
д 顢ṹ塢ָ ֮дõģ
ΪûǣЩ
ҾĻܻ¶δţ
Իȼطһºðɣ
֮ٲϵƺһ
## CԴdouble.c
#include <stdio.h>
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 еݼ
<pre><code>Double:<b>
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</b>
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp<b>
movl $1, 28(%esp)
movl 28(%esp), %eax
movl %eax, (%esp)
call Double</b>
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
</code></pre>
##
<b></b>Ƕ Double һá
ڴ main е 1 <b></b>ָʼ
<table border="1">
<tr><td><pre><code>
movl $1, 28(%esp) # a=1
</code></pre></td>
<td rowspan="2"><img src="http://fmn.rrimg.com/fmn056/20121109/1855/original_1Ykq_291900004dfe1191.jpg" />
</td></tr>
<tr><td>
Ǹ ֲ a ֵ<br>
ջָĴ esp ڵֵ 8000
ִָջͼ
</td></tr>
<tr><td><pre><code>
movl 28(%esp), %eax
movl %eax, (%esp) # (%esp) = a
</code></pre></td>
<td rowspan="2"><img src="http://fmn.rrimg.com/fmn063/20121109/1855/original_6CEt_3c0400004d8a125d.jpg" />
</td></tr>
<tr><td>
ָ a ֵ 1 д˵ַΪ 8000 ڴ飨4ֽڣ
b ɣ<br>ڻҪ½ۡ
</td></tr>
<tr><td><pre><code>
call Double
</code></pre></td>
<td rowspan="2"><img src="http://fmn.rrfmn.com/fmn058/20121109/1855/original_cK4p_448e000041101190.jpg" />
</td></tr>
<tr><td>
call ִָУɷΪ裺<br>
<ol>
<li> call ָһָĵַѹջ
<li> eip Ϊ Double ĵַ
</ol>
Ȼת Double ȡִָˡ<br>
call ָ֮ movl %eax, 24(%esp)
ĵַ 1000ô call ִк
ջΪͼ
</td></tr>
<tr><td><pre><code>
pushl %ebp
movl %esp, %ebp
</code></pre></td>
<td rowspan="2"><img src="http://fmn.rrfmn.com/fmn058/20121109/1855/original_5Rl9_5de000004db9118f.jpg" />
</td></tr>
<tr><td>
Ƚɵ ebp ѹջȻʱ esp ֵ ebp
ῴĿ
</td></tr>
<tr><td><pre><code>
subl $16, %esp # esp -= 16
</code></pre></td>
<td rowspan="2"><img src="http://fmn.rrimg.com/fmn056/20121109/1855/original_7ikk_0fad00004dc5118e.jpg" />
</td></tr>
<tr><td>
Double ľֲռôˣȻе˷ѣ
</td></tr>
<tr><td><pre><code>
movl 8(%ebp), %eax
addl %eax, %eax
movl %eax, -4(%ebp) # c = b + b
addl $1, 8(%ebp) # ++b
</code></pre></td>
<td rowspan="2"><img src="http://fmn.rrimg.com/fmn065/20121109/1855/original_VXpq_05be00004de5118d.jpg" />
</td></tr>
<tr><td>
8(%ebp) ǰպñʾ 8000 ڴ飬
4 ָ 8000 ڴֵеĴ
ǿȷ 8000 Dz b
-4(%ebp) c
</td></tr>
<tr><td><pre><code>
movl -4(%ebp), %eax # eax = c
leave
ret
</code></pre></td>
<td rowspan="2"><img src="http://fmn.rrimg.com/fmn056/20121109/1850/original_zq1t_433a00004e92125b.jpg" />
</td></tr>
<tr><td>
βˣ<b>ķֵҪ洢ۼӼĴ eax </b>
leave ָͬ<br>
movl %ebp, %esp<br>
popl %ebp<br>
ս Double ʱָβӦ<br>
<b>Ǿֲռ䱻ˣebp Ҳԭˡ</b>
<p> ret ൱ popl %eip
ͼִ call ָָ֮ˡ</p>
Ȼ Double ľֲռ䱻ˣ
еֵDZֲģһֱ֮ printf ʱ
Double ľֲԼݲűǡ
</td></tr>
</table>
##
һķٴӴϻعһ Double
<pre><code>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 #---------ջָָ֡롢
</code></pre>
ΪָҲл֡
<b> Double ̫ûгֱĴָ
eax ĴҪÿ֪淵ֵģ
ںֿ϶ᱻģӵĺںǰ
pushl ĵļĴ ret ֮ǰ popl ĴԻָԭֵ</b>
## С
ͨ Double ķ
ע ebp esp ƣ
оҲͨ ebp + ƫ ķʽ ֲ
ڿҳŵˣ<b>esp ebp ƫƾ
ֲıʽ</b>
ָ֡Ĵ 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]
<a name="top"></a>
<h1 align="center">汇编实现的动态栈
</h1>
这一篇就是实现 d_printf,废话不多说,直接上代码。
由于 VC 的内联汇编还是比较清晰,那就先贴 VC 版的。
## 一、d_printf VC版
#include <stdio.h>
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 <stdio.h>
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)。<b>由于转义符是编译时处理的,printf 是不管的,
所以\n什么的在这里不管用^_^</b>。
## 四、为什么用 static
d\_printf 中为什么将 size1、size2、fmt_copy 声明为
static 变量呢?
1. 不能用寄存器。size2 是在 call printf 之后使用的,
<b>但是后来我发现 printf 执行完后改动了好几个寄存器的值</b>,
所以如果将 size2 保存到某个寄存器中是不行的
(难怪C语言喜欢内存,寄存器太不可靠了o(╯□╰)o)。
2. 不能用局部变量。不让用寄存器就用内存呗!
<b>但是我们要自主修改 esp,
而局部变量有可能是通过 esp+常量偏移 定位的</b>
(如果是用 ebp+常量偏移(VC一般这么用)定位的就没问题),
所以 subl %6, %%esp 之后 到 addl %7, %%esp 之前
都不能使用局部变量,否则会定位错误。
所以才使用 static 变量,<b>
因为 static 变量是用绝对地址定位的,跟 esp 毫无关系</b>。
## 五、使用内联汇编的建议
1. 不要用。
2. 如果非得用的话,尽量用 C 语言实现+-*/,
如 d_printf 中给 size1、size2 赋值;
内联汇编只实现不得不用的部分(内联汇编一般都短小精悍)。
<h2 align="center">《解剖C语言》就此完结。</h2>
[回目录][content]
================================================
FILE: frame.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[回目录][content]
<a name="top"></a>
<h1 align="center">函数帧
</h1>
这标题一念出来我立刻想到了一个名人:白素贞……当然,
此女与本文无关,下面进入正题:
<pre>其实程序运行就好比一帧一帧地放电影,每一帧是一次函数调用,电影放完了,我们就看到结局了。</pre>
我们用一个递归求解阶乘的程序来看看这个放映过程(fac.c):
#include <stdio.h>
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 开始执行的):
<table>
<tr><td>
<pre><code>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
</code></pre></td>
<td><img src="http://fmn.rrimg.com/fmn056/20121124/1940/original_D0zG_726b00003e04118c.jpg" /></td>
</tr></table>
` `main 函数创建了一帧:
* 从 esp 到 ebp + 4
* 上边是本次调用的返回地址、旧的 ebp 指针
* 然后是 main 的局部变量 n、ans
* 最下边是参数的空间,右上图显示的是 main 中调用 printf
前的栈的使用情况
` `进入 main 函数,前 4 条指令开辟了这片空间,
在退出 main 函数之前的 leave ret 回收了这片空间
(<b>C++ 在回收这片空间之前要析构此函数中的所有局部对象</b>)。
<b>在 main 函数执行期间 ebp 一直指向 帧顶 - 4 的位置,
ebp 被称为帧指针也就是这个原因</b>。
## 调用惯例
调用函数的时候,先传参数,然后 call,
具体这个过程怎么实现有相关规定,这样的规定被称为<b>调用惯例</b>,
C语言中有多种调用惯例,它们的不同之处在于:
1. 参数是压栈还是存入寄存器
2. 参数压栈的次序(从右至左 | 从左至右)
3. 调用完成后是调用者还是被调用者来恢复栈
` `各种调用惯例<em>《程序员的自我修养》——链接、装载与库</em>
这本书中有简要介绍,我照抄后在本文后面列出。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:
<table>
<tr><td>
<pre><code>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
</code></pre></td>
<td><img src="http://fmn.rrfmn.com/fmn058/20121124/1940/original_XcDN_08c400005ab9125d.jpg" /></td>
</tr></table>
fac(3) 开辟了第一个 fac 帧:
* 从 esp 到 ebp + 4(fac 还能"越界"地读到参数 n)
* 上边是 返回地址、旧的 ebp 指针(指向 main 帧)
* fac 没有局部变量,又浪费了很多字节
* 参数占了最下边的 4 字节(需要递归时使用)
` `这时还不满足递归终止条件,于是fac(3)又递归地调用了fac(2),
fac(2)又递归的调用了fac(1),到这个时候栈变成了如下情况:

上图的箭头的含义很明显:
<b>从 ebp 可回溯到所有的函数帧</b>,
这是由于每个函数开头都来两条 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语言 能调来调去的保障。
<h2 align="center">主要的调用惯例</h2>
<table border="1">
<tr>
<th>调用惯例</th>
<th>出栈方</th>
<th>参数传递</th>
<th>名字修饰</th>
</tr>
<tr>
<td>cdecl</td>
<td>函数调用方</td>
<td>从右至左的顺序压参数入栈</td>
<td>下划线+函数名</td>
</tr>
<tr>
<td>stdcall</td>
<td>函数本身</td>
<td>从右至左的顺序压参数入栈</td>
<td>下划线+函数名+@+参数的字节数,
如函数 int func(int a, double b)的修饰名是
_func@12</td>
</tr>
<tr>
<td>fastcall</td>
<td>函数本身</td>
<td>头两个 DWORD(4字节)类型或者更少字节的参数
被放入寄存器,其他剩下的参数按从右至左的顺序入栈</td>
<td>@+函数名+@+参数的字节数</td>
</tr>
<tr>
<td>pascal</td>
<td>函数本身</td>
<td>从左至右的顺序入栈</td>
<td>较为复杂,参见pascal文档</td>
</tr>
</table>
[回目录][content]
================================================
FILE: gcc.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[Ŀ¼][content]
<a name="top"></a>
<h1 align="center">ͻ۽
</h1>
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
``<em> -o max2.c gcc
Ҫ max2.c ļ</em>
֣ΰɣ
# 1 "max.c"
# 1 "<built-in>"
# 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 <stdio.h>
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ǣ
Ԥ->->->
``ʹòͬıѡԵóͬм
<table border="1">
<tr>
<th></th>
<th></th>
<th>ضϺIJ</th>
</tr>
<tr>
<td></td>
<td></td>
<td>CԴ</td>
</tr>
<tr>
<td>Ԥ</td>
<td>gcc -E</td>
<td>滻˺CԴ(û#define,#include),
ɾע</td>
</tr>
<tr>
<td></td>
<td>gcc -S</td>
<td>Դ</td>
</tr>
<tr>
<td></td>
<td>gcc -c</td>
<td>Ŀļļ
вڴļеⲿ</td>
</tr>
<tr>
<td></td>
<td>gcc</td>
<td>ִгһɶĿļӶɣ
ļбҵõ</td>
</tr>
</table>
Ҳͬѧ -c һûأ
ļķҲõǺ٣õʱ˵ɡ
[Ŀ¼][content]
================================================
FILE: globalvar.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[Ŀ¼][content]
<a name="top"></a>
<h1 align="center">ȫֱ
</h1>
## ʵƷ
Сglobal.c
#include <stdio.h>
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 ļ
רҵĻ"ض"
<em>objdump linux һߣ
ܹĿļִļ</em>
global ļķ global.txt
ļУ global.txtļȽϳҵ 357 У
λ main
<pre><code>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<b>
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</b>
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 <printf@plt>
80483f1: b8 00 00 00 00 mov $0x0,%eax
80483f6: c9 leave
80483f7: c3 ret
</code></pre>
<b></b>ֱ ++i ķ
<b>ȫֱ i ˾ԵַΪ 0x8049664
ڴ飨СΪ4ֽڣ</b>
ע 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 أ
Ŀļ ִļˣ
<b>ȫֱĿļֻһðƵַ
ӺĿļЭ̣ΪԼȫֱһ̣
ϣյľԵַ</b>
## ĿļͿִļ
<b>ĿļDZIJ
Ѿ C ת</b>
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]
<a name="top"></a>
<h1 align="center">内联汇编
</h1>
内联汇编是指在 C/C++ 代码中嵌入的汇编代码,
与全部是汇编的汇编源文件不同,它们被嵌入到 C/C++ 的大环境中。
## 一、gcc 内联汇编
gcc 内联汇编的格式如下:
asm ( 汇编语句
: 输出操作数 // 非必需
: 输入操作数 // 非必需
: 其他被污染的寄存器 // 非必需
);
` `我们通过一个简单的例子来了解一下它的格式(gcc_add.c):
#include <stdio.h>
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行是汇编语句,用双引号引起来,
<b>多条语句用 ; 或者 \n\t 来分隔。</b>
2. 第2行是输出操作数,都是 "=?"(var) 的形式,
var 可以是任意内存变量(输出结果会存到这个变量中),
? 一般是下面这些标识符
(表示内联汇编中用什么来代理这个操作数):
* a,b,c,d,S,D 分别代表 eax,ebx,ecx,edx,esi,edi 寄存器
* r 上面的寄存器的任意一个(谁闲着就用谁)
* m 内存
* i 立即数(常量,只用于输入操作数)
* g 寄存器、内存、立即数 都行(gcc你看着办)
<b>在汇编中用 %序号 来代表这些输入/输出操作数,
序号从 0 开始。为了与操作数区分开来,
寄存器用两个%引出,如:%%eax</b>
3. 第3行是输入操作数,都是 "?"(var) 的形式,
<b>? 除了可以是上面的那些标识符,还可以是输出操作数的序号,
表示用 var 来初始化该输出操作数</b>,
上面的程序中 %0 和 %1 就是一个东西,初始化为 1(a的值)。
4. 第4行标出那些在汇编代码中修改了的、
又没有在输入/输出列表中列出的寄存器,
这样 gcc 就不会擅自使用这些"危险的"寄存器。
还可以用 "memory" 表示在内联汇编中修改了内存,
之前缓存在寄存器中的内存变量需要重新读取。
` `上面这一段内联汇编的效果就是,
把a与b的和存入了c。当然这只是一个示例程序,
谁要真这么用就蛋疼了,
<b>内联汇编一般在不得不用的情况下才使用</b>。
## 二、VC 内联汇编
gcc 内联汇编被设计得很复杂,初学者看了往往头大,
而 VC 的内联汇编就简单多了:
__asm{
汇编语句
}
` `一个例子程序如下(vc_add.c):
#include <stdio.h>
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 的内联汇编中可以直接以变量名的形式使用局部变量,
这就方便多了。<b>但是,
VC 内联汇编中有些变量名是保留的,比如:size,
使用这些变量名就会报错(把b改成size,
上面的程序就编译不通过了)。所以,起名字一定要小心!</b>
因为 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字节),
而是字节。
* <b>"=&D" (d1) 不是想将 edi 的最终值输出到 d1 中,
而是想告诉 gcc edi的值早就改了,
不要认为它的值还是初始化时的 dest,
避免"吝啬的" gcc 把修改了的 edi 还当做 dest 来用。</b>
而 d0、d1、d2 在开启优化后会被 gcc 无视掉
(输出到它们的值没有被用过)。
` `memcpy 先复制一个一个的双字,
到最后如果还有没复制完的(少于4个字节),
再一个一个字节地复制。
我最终实现的 d_printf 就模仿了这个函数。
<b>深入研究:</b><br>
<a target="_blank" href="http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html">gcc 内联汇编 HOWTO 文档</a><br>
<a target="_blank" href="http://lxr.free-electrons.com/ident">Linux Cross Reference——各版本 linux 内核函数检索</a>
[回目录][content]
================================================
FILE: localvar.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[Ŀ¼][content]
<a name="top"></a>
<h1 align="center">ֲ
</h1>
ڽļƪУ
ҽ"۽"һC
Ϊҽҿ C Եİء
##
һ죬һѷһֵ C
int i = 3;
int ans = (++i)+(++i)+(++i);
``˵ 18һΪ 4+5+6=15 ء
## ֤
ȻҾ֤һ½ģ
дµIJԳinc.c
#include <stdio.h>
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
ֻе<b></b>֣
<pre><code> .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<b>
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)</b>
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
</code></pre>
Ҳòһֻĸʽˣ
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ԡ
## С
Ӧȡ飺
<b>ʵʩݼıӦڱʽжγ</b>
ͲǿˣDZɷӣ
C 涨[е](http://blog.csdn.net/huiguixian/article/details/6438613)֮䣬
ִе˳ġ
˱ŻĿռ䡣[[1]](#tip1)
õ 15 ĻԸij
#include <stdio.h>
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]
<a name="tip1"></a>
[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]
<a name="top"></a>
<h1 align="center">奇怪的宏
</h1>
这一篇介绍这些奇怪的宏:
## 一、do while(0)
为了交换两个整型变量的值,前面<em>值传递</em>中已经用
包含指针参数的 swap 函数做到了,这次用<b>宏</b>来实现(swap.c):
#include <stdio.h>
#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) 是写了个循环
又不让它循环,蛋疼啊!其实不然,这样写是有妙用的:
<b>首先</b>,SWAP 有多条语句,如果这样写:
#define SWAP(a,b) \
int t = a; \
a = b; \
b = t;
` `那么用的时候就得这么用:
SWAP(c,d)
` `<b>不能加分号</b>!不习惯吧?
<b>其次</b>,使用 do{...}while(0),
中间的语句用大括号括起来了,所以是另一个命名空间,
<b>其中的新变量 t 不会发生命名冲突</b>。
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);
` `需要注意的是<b>C语言中多个双引号字符串放在一起
会自动连接起来</b>,所以如果 divider 为 0 的话,就会打印出:
Warning: divider == 0
## 三、##
# 还是比较少用的,## 却比较流行,
在 linux0.01 中就用到过。## 被称为连接符,
用来将两个 记号(编译原理中的词汇) 连接为一个 记号。
看下面的例子吧(add.c):
#include <stdio.h>
#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; }
<b>以上内容都可以使用<em>照妖镜</em>看到宏被替换后的情形</b>。
[回目录][content]
================================================
FILE: main.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[回目录][content]
<a name="top"></a>
<h1 align="center">谁调用了main?
</h1>
这是函数帧的应用之一。
## 操作可行性
从上一篇中可以发现:用帧指针 ebp 可以回溯到所有的函数帧,
那么 main 函数帧之上的函数帧自然也是可以的;
而帧中 旧ebp 的上一个四字节存的是函数的返回地址,
由这个地址我们可以判断出谁调用了这个函数。
## 准备活动
下面就是这次黑客行动的主角(up.c):
#include <stdio.h>
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(上一帧的帧指针);
这样,程序将按照<b>调用顺序的逆序</b>打印出各个返回地址。
为什么终止条件是 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 通过<b>管道</b>将 nm up 的输出作为 sort 的输入,
sort 排序后<b>输出重定向</b>到 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 函数之前程序都干了些啥事呢?
比如说<b>堆的初始化</b>,如果是 C++ 程序的话,
全局对象的构造也是在 main 之前完成的
(不能让 main 中使用全局对象的时候竟然还没构造吧!),
而全局对象的析构也相当有趣地在 main 执行完了之后才执行。
main 在你心目中的地位是不是一落千丈了?
[回目录][content]
================================================
FILE: mem.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[回目录][content]
<a name="top"></a>
<h1 align="center">进程内存分布
</h1>
之前一直在分析栈,栈这个东西的作用也介绍得差不多了,
但是栈在哪儿还没有搞清楚,以及堆、代码、全局变量它们在哪儿,
这都牵涉到进程的内存分布。
## linux 0.01 的进程内存分布
内存分布随着操作系统的更新换代,越来越科学合理,
也越来越复杂,所以我们还是先了解一下早期操作系统的典型
linux 0.01 的进程的内存分布:
linux 0.01 的一个进程固定拥有64MB的线性内存空间
(ACM竞赛中单个程序的最大内存占用限制为64MB,
这肯定有猫腻O(∩_∩)O~),各个进程挨个放置在一张页目录表中,
一个页目录表可管理4G的线性空间,因此 linux0.01 最多有
64个进程。每个进程的内存分布如下:

* .text 里存的是机器码序列
* .rodata 里存的是源字符串等只读内容
* .data 里存的是初始化的全局变量
* .bss 上一篇介绍过了,存的是未初始化的全局变量
* 堆、栈就不用介绍了吧!
` `.text .rodata .data .bss 是常驻内存的,
<b>也就是说进程从开始运行到进程僵死它们一直蹲在那里,
所以访问它们用的是常量地址</b>;而栈是不断的加帧(函数调用)
、减帧(函数返回)的,帧内的局部变量只能用相对于当前
esp(指向栈顶)或 ebp(指向当前帧)的相对地址来访问。
栈被放置在高地址也是有原因的:
<b>调用函数(加帧)是减 esp 的,函数返回(减帧)是加 esp 的,
调用在前,所以栈是向低地址扩展的,放在高地址再合适不过了</b>。
## 现代操作系统的进程内存分布
认识了 linux 0.01 的内存分布后,
再看看现代操作系统的内存分布发生了什么变化:
<b>首先</b>,linux 0.01 进程的64MB内存限制太过时了,
现在的程序都有潜力使用到 2GB、3GB 的内存空间
(每个进程一张页目录表),当然,机器有硬伤的话也没办法,
我的电脑就只有 2GB 的内存,想用 3GB 的内存是没指望了。
但也不是有4GB内存就可以用4GB(32位),
因为操作系统还要占个坑呢!
现代 linux 中 0xC0000000 以上的 1GB 空间是操作系统专用的,
而 linux 0.01 中第1个 64MB 是操作系统的坑,
所以别的进程完全占有它们的 64MB,
也不用跟操作系统客气。
<b>其次</b>,linux 0.01只有进程没有线程,
但是现代 linux 有多线程了
(linux 的线程其实是个轻量级的进程),
<b>一个进程的多个线程之间共享全局变量、堆、打开的文件……
但栈是不能共享的</b>:栈中各层函数帧代表着一条执行线索,
一个线程是一条执行线索,所以每个线程独占一个栈,
而这些栈又都必须在所属进程的内存空间中。
根据以上两点,进程的内存分布就变成了下面这个样子:

再者,如果把动态装载的动态链接库也考虑进去的话,
上面的分布图将会更加"破碎"。
<b>如果我们的程序没有采用多线程的话,
一般可以简单地认为它的内存分布模型是 linux 0.01 的那种</b>。
[回目录][content]
================================================
FILE: name.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[回目录][content]
<a name="top"></a>
<h1 align="center">变量名、函数名
</h1>
C程序在执行的时候直接用内存地址去定位变量、函数,
而不是根据名字去搜索,所以C程序执行的速度比脚本语言要快不少。
对于函数中的局部变量来说,编译为汇编的时候,
名字就已经被彻彻底底地忘记了,
因为局部变量在函数帧中,这一帧要占多少字节,
各局部变量在帧中的相对位置,
都在编译成汇编的时候就可以确定下来,
生成目标文件、可执行文件的时候也不需要再更改。
而 全局变量、static变量、函数 由于要将所有目标文件、
库链接到一起之后才能最终确定它们的绝对地址,
所以在链接前名字还是标志着它们的存在。
它们的信息存储在符号表(符号数组)中,
其中每一项除了有符号名,还有符号地址(链接后填入),
所以 nm 命令可得到 地址-符号名 映射。
虽然程序运行时用不到符号表,
但是默认情况下可执行文件中还是存着符号表,
看下面这个程序(name.c):
#include <stdio.h>
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]
<a name="top"></a>
<h1 align="center">编译优化
</h1>
C语言没有汇编快,因为C语言要由编译器翻译为汇编,
编译器毕竟是人造的,翻译出来的汇编源代码总有那么N条
指令在更智能、更有创造性的我们看来是多余的。
C语言翻译后的汇编有如下恶劣行径:
1. <b>C语言偏爱内存</b>。我们写的汇编一般偏爱寄存器,
寄存器比内存要快很多倍。当然,寄存器的数量屈指可数,
数据多了的话也必须用内存。
2. <b>内存多余读</b>。假如在一个 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 <stdio.h>
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]
<a name="top"></a>
<h1 align="center">函数指针
</h1>
## 一、函数指针的值
函数指针跟普通指针一样,存的也是一个内存地址,
只是这个地址是一个函数的起始地址,
下面这个程序打印出一个函数指针的值(func1.c):
#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
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,
所以这个程序依然顺利地执行完了。
<b>综上所述,函数指针真没什么特别的。</b>
[回目录][content]
================================================
FILE: process0.01.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[Ŀ¼][content]
<a name="top"></a>
<h1 align="center">linux0.01ʱƬĺ
</h1>
ǡlinuxϵеĵһƪΪʲôҪӽ̿ʼأ
Ϊϵͳš͵ľǽ̣
Խ̵ĹҪIJ־ǶʱƬĴ
<b>ⲿֵ㷨Ӱ쵽лٶȣ
DzϵͳһҪָ꣩</b>
ͬʱֳ<b>ϵͳ</b>
ǡϵͳἰ
---
ȣвϵͳӦеԣ
1. ÿ̶ᱻɸʱƬʱƬ꣬
<b>ʱ</b>ͲͶ
2. ÿ̶ȼȼߵĽ̷϶ʱƬ
3. ʱƬûսᣬӦٷʱƬ
ܲһ̱ʱɰ^_^
``linux0.01<b>һʱƬ10</b>0.01룩
̵ȼ1~15<b>ȼ·
ʱƬʱ̻õʱƬ</b>
<b>ôʱƬôĵأ</b>
ﲻòǵĴ8253
ûԭϽĿɱ̶ʱ/
<b>ÿ10msһʱжϣ
жϳаѵǰ̵ʱƬ1</b>
ʱƬĵġ
˵жϣһҲϤģǾ8259жϿ
оƬͲ˵ˣ߾Ϳʼᷳƪ
O(_)O~
---
ǰ̵ʱƬ<b>0</b>֮ͻschedule
лһеʣʱƬḶ̌
Ҫн̣ǸO(n)ʱ临ӶȵĹ
n̵ĸ
*ע⣺һ£ǰֻԼеʱƬ֮
scheduleÿһʱƬ͵һschedule
ʹˡ*
ģһ½̵ĵ
1. 3ֱ̣ʣʱƬ543ȼ15
ʱ schedule
ô1ᱻѡΪҪִеḶ̌

2. ִеһ50msĹ1ʱƬ

3. ʱʱжϳлscheduleл2ִУ
ǽ2ִ20msڵȴIOˣ

4. ߵĺлscheduleл3ִУ
һֱ3ʱƬҲȫ꣺

5. ڽ1ͽ3ûʱƬ˲ͶУ
2˲ͶУ
scheduleϣͨʱƬΣ
ÿµʱƬļ㹫ʽǣ
µʱƬ = ɵʱƬ / 2 + ȼ
ʽӿе֣ɵʱƬӦö0
ģпԿߵĽܻ̿ʱƬ
ߵĽ̾ͱرˣ

ߵḺ̌رյԭIOƵ
ı༭״̬
رյĻѱscheduleѡУ
ʱƬʱ2ԭ2ʱƬ䣬
ʱƬպñѣʱ1ͽ3ʱƬ15
࣬Ҳ˵ҪȽ1ͽ3̱֮
2ܱͶСֻ2CPUḶ̌
300ms2ˣĽ20أ
Ǿ͵3sǴֵλҪðˣ
Ĺ̻һʱϣģ

---
## ܽ
``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]
<a name="top"></a>
<h1 align="center">linux2.6.XXлʱƬ
</h1>
һƪнlinux0.01Ľлʱ临ӶO(n)ģ
linux0.01˵ʲô⣬
Ϊlinux0.01ֻ64̡
ڵlinuxϵͳмǧִ̲У
Ϊ˸ܵصȽ̣ʱƬ㷨ҲԽԽӣ
ȻO(n)л㷨ǾеΣˡ
һƪпԿʱҪط
1. ѡʵĽ̽лO(n)
2. ʱƬO(n)
``<b>linux2.6.XXгɹİǽO(1)</b>
---
# ѡ̽л
linux2.6.XX иװ˽ϢĽṹ壺
ȼ(struct prio_array)ṹ3Ա
1. ̸
2. ȼλͼ
3. ȼе
``<b>λͼÿһλӦһȼУ
зǿգǶӦһλ1</b>

linux2.6.XX Ľ140ȼ
ԶӦλͼӦ140أʵ5unsigned long
ɣ160ءlinux0.01ͬ2.6ȼ
ֵԽСȼԽߡ2.6ִʣʱƬḶ̌
ִȼߵĽ̡
<b>ԵȵʱֻҪҳ1Ϊ1λ
ȻӸλӦĶгһ̾Ϳˡ</b>
ѰҵһΪ1λbsflָ
һο4ֽڣ۴ڶٽָ౻ִ5Σ
㿪˵
ǰλͼôڿеĽ
ȼ7ֻҪ7гһ̣лˡ
---
# ʱƬ
<b>ǰ̵ʱƬĹ֮·ʱƬ</b>
ʱƬúٷõԭȼ
ȼĽ̾͵õȸȼĽ˲ͶУ
ͲǶˣ
ʵһȼ飨Ϊexpired
֮ǰ̸۵һֱactiveȼ飩
ٷʱƬĽ̶õexpired֮С
һʱ
activeеĽһתƵexpired֮У
ʱscheduleύһactiveexpired
ؽһָͺˣ
ȻֿԴactiveѡˡ
ͼIJïɣ
1. ԭ3̣ABCǵȼֱ141616

2. ȼAᱻִУAʱƬˣ
Aᱻ·ʱƬõexpiredУ

3. BCȼߵģB迿λͼǶͷ
ʵ˫ҾͲˣBִ

4. C

5. activeѾûнˣָ룺

``<b>ÿһôļ
̵лʱ俪Ѿ̵û̫Ĺϵˡ</b>
---
ʵʱƬٷ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]
<a name="top"></a>
<h1 align="center">所有递归都可以变循环
</h1>
这是函数帧的应用之二。
还记得大一的C程序设计课上讲到汉诺塔的时候老师说:
<b>所有递归都可以用循环实现</b>。这听起来好像可行,
然后我就开始想怎么用循环来解决汉诺塔问题,
我大概想了一个星期,最后终于选择了……放弃……
当然,我不是来推翻标题的,
随着学习的深入,以及"自觉修炼",现在我可以肯定地告诉大家:
所有递归都可以用循环实现,更确切地说:
<b>所有递归都可以用循环+栈实现</b>
(就多个数据结构,还不算违规吧O(∩_∩)O~)。
<b>通过在我们自定义的栈中自建函数帧,
我们可以达到和函数调用一样的效果</b>。
但是因为这样做还是比较麻烦,所以就不转换汉诺塔问题了,
而是转换之前的那个递归求解阶乘的程序(fac.c):
#include <stdio.h>
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;
}
## 技术难点
我们可以在自建的函数帧中存储局部变量、存储参数,
<b>但是我们不能存返回地址,因为我们得不到机器指令的地址!</b>
不过,C语言有一个类似于指令地址的东西:switch case
中的 case子句,我们可以用一个case代表一个地址,
技术难点就此突破了。
## 源程序
虽然我简化了很多步骤,但源程序还是比较长(fac2.c):
#include <stdio.h>
// 栈的设置
#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 个字节:

在计算到 fac(1) 的时候,栈中内容如下:

比起肆意挥霍栈空间的 gcc(fac帧用了32字节,
浪费了20字节,实际使用了12字节),
我们的程序真的是太节省了(一帧只用8字节)。
## 小结
当然,本文的方法只用于学术讨论,
说明所有递归都可以变循环,编程的时候还是不要这么用。
因为代码复杂、容易出错、难以理解,
唯一的优点是能省空间省到极限。
这种递归变循环的方式并没有降低时间复杂度,
但却是通用的(所有递归都可以这么变循环);
而有一部分递归可以基于巧妙的算法变成循环,
并且大大降低时间复杂度,如:动态规划、贪心算法
(详见《算法导论》)。
[回目录][content]
================================================
FILE: static.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[回目录][content]
<a name="top"></a>
<h1 align="center">static变量 及 作用域控制
</h1>
## 一、static变量
<b>static变量放在函数中,就只有这个函数能访问它;
放在函数外就只有这个文件能访问它。</b>
下面我们看看两个函数中重名的static变量是怎么区别开来的
(static.c):
#include <stdio.h>
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变量就容易区分了。
<b>其实static变量跟全局变量一样被放置在 .data段 或
.bss段 中,所以它们也是程序运行期间一直存在的,
最终也是通过绝对地址来访问</b>。
但是它们的作用域还是比全局变量低了一级:
static变量被标识为LOCAL符号,全局变量被标识为GLOBAL符号,
在链接过程中,目标文件寻找外部变量时只在GLOBAL符号中找,
所以static变量别的源文件是"看不见"的。
## 二、作用域控制
作用域控制为的是提高源代码的可读性,
一个变量的作用域越小,它可能出没的范围就越小。
C语言中的变量按作用域从大到小可分为四种:
全局变量、函数外static变量、函数内static变量、局部变量:
1. 全局变量是杀伤半径最大的:<b>不仅在定义该变量的源文件中可用,
而且在任一别的源文件中只要用 extern 声明它后也可以使用</b>,
因此,当你看到一个全局变量的时候应该心生敬畏!
2. 函数外的static变量处于文件域中,
只有定义它的源文件中可以使用。如果你看到一个static变量,
那是作者在安慰你:哥们(妹子),这个变量不会在别的文件中出现。
3. 函数内static变量在函数的<b>每次调用</b>中可用(只初始化一次),
<b>它同以上两种变量一样在程序运行期间一直存在</b>,
所以它的功能是局部变量无法实现的。
4. 局部变量在函数的<b>一次调用</b>中使用,
调用结束后就消失了。
` `显然,作用域越小越省心,
该是局部变量的就不要定义成全局变量,
如果"全局变量"只在本源文件中使用那就加个static。
即便是局部变量也还可以压缩其作用域:
有的同学写的函数一开头就声明了函数中要用到的所有局部变量,
一开始我也这么做,因为我担心:<b>如果把变量定义在循环体内,
是不是每一次循环都会给它们分配空间、回收空间,从而降低效率?</b>
但事实是它们的空间在函数的开头就一次性分配好了(scope.c):
#include <stdio.h>
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一次性分配好的。
<b>由此可见不是每个{}都要分配回收局部变量,
一个函数只分配回收一次</b>。因此,
如果某个变量只在某个条件、循环中用到的话,
还是在条件、循环中定义吧,这样,
规模比较大的函数的可读性将提高不少,而效率丝毫没有下降,
可谓是百利而无一害!
[回目录][content]
================================================
FILE: staticstack.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[回目录][content]
<a name="top"></a>
<h1 align="center">C语言的栈是静态的
</h1>
C语言有了可变参数之后,我们可以传任意个数的参数了,
似乎挺动态的了,但是可变参数函数还是不够动态。
## 一、鞭长莫及
我们可以在 main 中写出好几条参数个数不同的调用 sum 的语句,
但是具体到某一条语句,sum 的参数个数是一定的,
比如上一篇中的 sum(2, 3, 4) 的参数个数是 3。
<b>如果程序运行中调用 sum 函数的时候,
参数个数根据用户输入而定,那就不能用可变参数来实现了。</b>
也就是说不能用 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 的语句又必须明确的列出所有参数。
其根本原因是<b>C语言的栈是静态的</b>,
上一篇的 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]
<a name="top"></a>
<h1 align="center">ַ
</h1>
һƪַַʹã
Ҳ٣
## һַĴ洢λ
CԴstring1.c
#include <stdio.h>
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£
<pre><code>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<b>
80483bd: c7 04 24 94 84 04 08 movl $0x8048494,(%esp)
80483c4: e8 27 ff ff ff call 80482f0 <puts@plt></b>
80483c9: b8 00 00 00 00 mov $0x0,%eax
80483ce: c9 leave
80483cf: c3 ret
</code></pre>
ɼ 0x8048494 Ӧ "Hello, World!" ĵַ
Ȼ .rodata 0x8048488 + 12 ô洢
"Hello, World!" ĸַ
<a target="_blank" href="http://www.asciitable.com/">ASCII</a>
'H' <a target="_blank" href="http://www.asciitable.com/">ASCII</a> 0x48
̾ '!' <a target="_blank" href="http://www.asciitable.com/">ASCII</a> 0x21
ԶΪ˸ַ 0x00
<b>ֻҪ˫ַԶΪǼӽ</b>
ɴǷ֣
<b>ַȫֱһΪ̬ݴ洢ڿִļУ
ʹõʱóַʡ</b>
ַ .rodata У
е .text ΣΣеһ
ڿִļڴʱ
ֻģֻͨҳʵֵģ
ҳһλΪ 1 ʾҳд
Ϊ 0 ʾҳֻͼֻҳдݣ
CPU ͻᴥҳ쳣
<b>Դַеκַڳʱ</b>
## ַָ ַ
CԴstring2.c
#include <stdio.h>
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"ĵַ
ԣ<b>ֵַָʱݵԴַĵַ
ֲֵַʱҪһֲռ</b>
˵2ַʽ˷ѿռ䣨4ֽ vs 8ֽڣ
˷ʱ䣨1mov vs 4movǵ2ַʽҲ
һǴ<b>1ַʽݵԴַĵַ
ԴַֻҳУģַȴ</b>
飺бûĶַ
Ǿָɣ ַ ̬Ŀռ 档
## ʽ ת
ַ֣иʽתȥ
CԴstring3.c
#include <stdio.h>
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..
``֪ˣ<b>תڱɶļ
ͱתΪǸַ</b>
ʽȻҪ printf ʱõġ
֪ʲôã printf
ôתַǾͲòˣ
תȥġ
뿴 printf ŵʵ֣
linux 0.01 kernel/vsprintf.c һ
ûʵָĴʵ֣תַDzĵġ
linux 汾ںԴ룺<a target="_blank" href="http://www.kernel.org/pub/linux/kernel/">http://www.kernel.org/pub/linux/kernel/</a>
linux 0.01<a target="_blank" href="http://www.kernel.org/pub/linux/kernel/Historic/">http://www.kernel.org/pub/linux/kernel/Historic/</a>
[Ŀ¼][content]
================================================
FILE: struct.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[回目录][content]
<a name="top"></a>
<h1 align="center">结构体
</h1>
结构体是 C 语言主要的自定义类型方案,
这篇就来认识一下结构体。
## 一、结构体的形态
C源程序(struct.c):
#include <stdio.h>
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个字节
` `不得不感叹名字(结构体名字、子元素名字)再一次被抛弃了,
子元素名代表的是相对于结构体的偏移。
## 二、结构体的复制
大一的时候,老师千叮咛万嘱咐:<b>数组不能复制!</b>,
但是当发现下面这个程序正常运行后,我困惑了(block.c):
#include <stdio.h>
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 复制了一个"数组"。
其原因是:结构体是可以复制的,
结构体又可以包括任意类型的子元素,数组也行,
所以"数组"也被复制了。
那为什么纯粹的数组就不能复制呢?
我们可以这样去理解:<b>一个变量能被复制的必要条件是
我们知道它的大小</b>。结构体做为自定义类型,
在编译的时候编译器必然存储了它的子元素类型、个数等相关信息,
结构体的大小也就知道了;而数组一般只在乎它的类型和
起始地址,元素个数总是被忽视的(例如:
void func(char s[]) 可接受任何长度的字符数组做参数),
而且元素个数也没有被当做数组的一部分存入内存,
所以数组的复制是不好实现的。
## 小结
如果给结构体下一个实在点的定义话,那就是:
<b>有格式的字节数组</b>。有了结构体后 C 语言的
变量类型就丰富多了,但是同时也要注意:
1. 超过 4 字节的结构体不宜做参数(参数传递浪费时间、空间),
换做指针更好。
2. 超过 4 字节的结构体不宜做返回值类型
(话说一般返回值都用 eax 来存,
那么超过 4 字节的时候怎么存呢?自己去探索吧!)。
[回目录][content]
================================================
FILE: varargs.md
================================================
[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content
[回目录][content]
<a name="top"></a>
<h1 align="center">可变参数
</h1>
C语言的可变参数的实现非常巧妙:
大师只用了 3 个宏就解决了这个难题。
## 一、可变参数的应用
这里实现一个简单的可变参数函数 sum:
它将个数不定的多个整型参数求和后返回,
其第 1 个参数指明了要相加的数的个数(va.c):
#include <stdio.h>
#include <stdarg.h>
// 要相加的整数的个数为 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 的栈是向低地址增长的,
所以参数的排列顺序如下:

va\_start(n, ap)
就是 ( ap = (char*)(&n) + 4 )
因此 ap 被赋值为 ebp+12 也就是变参列表的起始地址。
之后 va\_arg 取出每一个参数:
( *(int *)((ap += 4) - 4) )
它首先将变参指针 ap 右移到下一个参数的起始地址,
再将加赋操作的返回值减到之前的位置取出一个参数。
这样,<b>用一条语句既取出了当前参数,又后移了指针 ap</b>,
真是神了!
sum 中循环使用 va\_arg 就取出了 n 个要相加的整数。
## 三、变参函数的可行性
一个变参函数能接受个数、类型可变的参数,
需要满足以下两个条件:
1. 能定位到可变参数列表的起始地址
2. 能获知可变参数的个数、每个参数的大小(类型)
` `条件 1 只要有个前置参数就能满足,
而对于这样的变参函数:void func(...);
编译能通过,但是不能用 va_start 取到变参列表的起始地址,
所以基本不可行。
<hr width="50%">
sum 函数中参数 n 被用来定位可变参数列表的起始地址
(满足条件1);n 的值是可变参数的个数,
类型默认全部是 int 型(满足条件2),
因此 sum 能正常工作。
<hr width="50%">
再看看 printf 函数是如何满足以上两个条件的,
printf 函数的原型是:
int printf(const char *fmt, ...);
` `printf 的第1个参数 fmt(格式串)被用来定位其后
的可变参数的起始地址(满足条件1);
fmt 指向的字符串中的各个格式描述符如:%d、%lf、%s 等
告诉了 printf fmt 之后参数的个数、各个参数的类型
(满足条件2),因此 printf 能正常工作。
<hr width="50%">
当然,sum、printf 能正常工作是设计者一厢情愿的期望,
如果使用者不按规矩传入参数、格式串,函数能正常工作才怪!
比如:
sum(2, "111", "222");
printf("%s", 0);
` `编译器可不会进行可变参数的类型检查、格式串-参数匹配,
后果将会在运行的时候出现……
[回目录][content]
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
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (98K chars).
[
{
"path": "README.md",
"chars": 4027,
"preview": "<a name=\"top\"></a>\n\n<h1 align=\"center\"><b>朴素linux</b></h1>\n\n 大学里我坚持的最久的一项任务就是自学 linux 内核,\n虽然以后可能也没机会从事 linux 内核方面的工作,\n"
},
{
"path": "align.md",
"chars": 2573,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "array.md",
"chars": 2429,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[Ŀ¼][content]\n\n<a name=\"top\"></a>\n\n<"
},
{
"path": "bss.md",
"chars": 1804,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "byval.md",
"chars": 948,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[Ŀ¼][content]\n\n<a name=\"top\"></a>\n\n<"
},
{
"path": "call.md",
"chars": 3861,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[Ŀ¼][content]\n\n<a name=\"top\"></a>\n\n<"
},
{
"path": "dynamicstack.md",
"chars": 3205,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "frame.md",
"chars": 3695,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "gcc.md",
"chars": 1715,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[Ŀ¼][content]\n\n<a name=\"top\"></a>\n\n<"
},
{
"path": "globalvar.md",
"chars": 2405,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[Ŀ¼][content]\n\n<a name=\"top\"></a>\n\n<"
},
{
"path": "inlineasm.md",
"chars": 3394,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "localvar.md",
"chars": 2486,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[Ŀ¼][content]\n\n<a name=\"top\"></a>\n\n<"
},
{
"path": "macro.md",
"chars": 1842,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "main.md",
"chars": 1862,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "mem.md",
"chars": 1680,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "name.md",
"chars": 1984,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "optimize.md",
"chars": 1662,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "pfunc.md",
"chars": 2102,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "process0.01.md",
"chars": 1582,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[Ŀ¼][content]\n\n<a name=\"top\"></a>\n\n<"
},
{
"path": "process2.6.md",
"chars": 1672,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[Ŀ¼][content]\n\n<a name=\"top\"></a>\n\n<"
},
{
"path": "recur.md",
"chars": 2193,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "static.md",
"chars": 2071,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "staticstack.md",
"chars": 1461,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "string.md",
"chars": 3205,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[Ŀ¼][content]\n\n<a name=\"top\"></a>\n\n<"
},
{
"path": "struct.md",
"chars": 1789,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
},
{
"path": "varargs.md",
"chars": 2341,
"preview": "[content]: https://github.com/1184893257/simplelinux/blob/master/README.md#content\n\n[回目录][content]\n\n<a name=\"top\"></a>\n"
}
]
About this extraction
This page contains the full source code of the 1184893257/simplelinux GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (58.6 KB), approximately 33.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.