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 ִָУɷΪ裺
- call ָһָĵַѹջ
- 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),到这个时候栈变成了如下情况:

上图的箭头的含义很明显:
从 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 |
|
|
CԴ |
| Ԥ |
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个进程。每个进程的内存分布如下:

* .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 的线程其实是个轻量级的进程),
一个进程的多个线程之间共享全局变量、堆、打开的文件……
但栈是不能共享的:栈中各层函数帧代表着一条执行线索,
一个线程是一条执行线索,所以每个线程独占一个栈,
而这些栈又都必须在所属进程的内存空间中。
根据以上两点,进程的内存分布就变成了下面这个样子:

再者,如果把动态装载的动态链接库也考虑进去的话,
上面的分布图将会更加"破碎"。
如果我们的程序没有采用多线程的话,
一般可以简单地认为它的内存分布模型是 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ᱻѡΪҪִеḶ̌

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]
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

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

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

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

4. C

5. activeѾûнˣָ룺

``ÿһôļ
̵лʱ俪Ѿ̵û̫Ĺϵˡ
---
ʵʱƬٷ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 个字节:

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

比起肆意挥霍栈空间的 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 的栈是向低地址增长的,
所以参数的排列顺序如下:

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]