*本章是理解程序如何运行的关键所在,这一章将学到:  
程序时如何从高级代码变为汇编代码;    
程序编译后运行时在内存中是如何分布的; 3…* 
 
 
程序代码 首先介绍各种单片机、处理器,不同的处理器所使用的能够识别的汇编语言也是不一样的,如何根据不同处理器把同一段高级语言转换成对应的汇编语言,这个是编译器的事儿,本文所有的汇编语言我的机器就跟他不同,导致我学习的时候也为此很头疼,严重影响了我对本来就一知半解的汇编语言的理解。
程序编码 在深入了解程序代码之前需要简单的了解一下GCC编译器的使用方法。
首先编写一个简单的C文件:code.c1 2 3 4 5 6 7 8 9  //code.c int accum = 0;    int sum(int x, int y) {     int t = x + y;     accum += t;     return t; } 
然后利用gcc命令加上-S参数生产一个汇编代码文件:1 2 3 4 gcc -O1 -S code.c ll code.c code.s 
 可见生成了一个.s文件,打开这个汇编文件,内容如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22         .file"code.c"         .text .globl sum         .type   sum, @function sum: .LFB0:         .cfi_startproc         leal    (%rsi,%rdi), %eax         addl    %eax, accum(%rip)         ret         .cfi_endproc .LFE0:         .size   sum, .-sum .globl accum         .bss         .align 4                             .type   accum, @object         .size   accum, 4 accum:         .zero4         .ident  "GCC: (GNU) 4.4.6 20110731 (Red Hat 4.4.6-3)"         .section    .note.GNU-stack,"",@progbits 
 但是书中给出的生成的代码却是这样的:1 2 3 4 5 6 7 8 sum:     pushl   %ebp     movl    %esp, %ebp     movl    12(%ebp), %eax     addl    8(%ebp), %eax     addl    %eax, accum     popl    %ebp     ret 
 可以看到这两段代码差别很大,这是因为所在的处理器环境不同,所以汇编命令格式会有差别;而且gcc的版本也不同,会导致优化的程度也不同。为了能够更好的理解课本,以后就按照书上的来理解。 如果在使用gcc的时候加-C参数,则汇编器会把汇编文件编译成二进制文件: 这是生产了code.o文件,但是它的内容是二进制的,直接看不到。用vi打开文件,然后在normal模式下输入::%!xxd1 2 3 4 5 6 7 8 9 10 11 0000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............ 0000010: 0100 3e00 0100 0000 0000 0000 0000 0000  ..>............. 0000020: 0000 0000 0000 0000 1001 0000 0000 0000  ................ 0000030: 0000 0000 4000 0000 0000 4000 0c00 0900  ....@.....@..... 0000040: 8d04 3e01 0500 0000 00c3 0000 0047 4343  ..>..........GCC 0000050: 3a20 2847 4e55 2920 342e 342e 3620 3230  : (GNU) 4.4.6 20 0000060: 3131 3037 3331 2028 5265 6420 4861 7420  110731 (Red Hat 0000070: 342e 342e 362d 3329 0000 0000 0000 0000  4.4.6-3)........ 0000080: 1400 0000 0000 0000 017a 5200 0178 1001  .........zR..x.. 0000090: 1b0c 0708 9001 0000 1400 0000 1c00 0000  ................ ...... 
 还可以通过gdb这个调试工具进行查看二进制文件:1 2 3 4 $gbd code.o (gdb) x/17xb sum 0x0 <sum>:0x8d0x040x3e0x010x050x000x000x                                00 0x8 <sum+8>:0x000xc3Cannot access memory at address 0xa          
 但是要查看目标代码文件的内容,最有价值的还是反汇编器(disassembler),这玩意儿听上去就很NB有木有?使用方法入下:1 2 3 4 5 6 7 8 9 10 objdump -d code.o code.o:     file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <sum>:    0:8d 04 3e             lea    (%r        si,%rdi,1),%eax    3:01 05 00 00 00 00    add    %eax,0x0(%rip)               # 9 <sum+0x9>    9:c3                   retq       
 这个是我电脑上的结果,跟书上的有不一样,书上的是:1 2 3 4 5 6 7 8 9 10 11 Disassembly of funciton sum in binary file code.o 00000000 <sum> Offset  Bytes                   Equivalent assembly language     0:  55                      push    %ebp     1:  89  e5                  mov %esp,%ebp     3:  8b  45  0c              mov 0xc(%ebp),%eax     6:  03  45  08              add 0x8(%ebp), %eax     9:  01  05  00  00  00  00  add %eax,0x0     f:  5d                      pop %ebp    10:  c3                      ret 
 生成实际可执行的代码需要对一组目标代码文件运行链接器,而这一组目标代码文件中必须含有一个main函数,假设文件main.c中有下面这样一个函数:1 2 3 4 int main() {     return sum(1,3) } 
 然后用下面的方法生成可执行文件prog:1 gcc -O1 -o prog code.o main.c 
 看到生成了一个prog可执行文件,对其进行反编译 可看到一大坨的汇编代码,但是其中有一段跟上面的代码一样
1 2 3 4 5 6 7 8 9 10 objdump -d code.o code.o:     file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <sum>:    0:8d 04 3e             lea    (%rsi,%rdi,1),%eax    3:01 05 00 00 00 00    add    %eax,0x0(%rip)               # 9 <sum+0x9>    9:c3                   retq       
 
这个是我电脑上的结果,跟书上的有不一样,书上的是:1 2 3 4 5 6 7 8 9 10 11 Disassembly of funciton sum in binary file code.o 00000000 <sum> Offset  Bytes                   Equivalent assembly language     0:  55                      push    %ebp     1:  89  e5                  mov %esp,%ebp     3:  8b  45  0c              mov 0xc(%ebp),%eax     6:  03  45  08              add 0x8(%ebp), %eax     9:  01  05  00  00  00  00  add %eax,0x0     f:  5d                      pop %ebp    10:  c3                      ret 
 生成实际可执行的代码需要对一组目标代码文件运行链接器,而这一组目标代码文件中必须含有一个main函数,假设文件main.c中有下面这样一个函数:1 2 3 4 int main() {     return sum(1,3) } 
 然后用下面的方法生成可执行文件prog:1 gcc -O1 -o prog code.o main.c 
 看到生成了一个prog可执行文件,对其进行反编译 可看到一大坨的汇编代码,但是其中有一段跟上面的代码一样1 2 3 4 5 6 0000000000400474 <sum>:   400474:   8d 04 3e             lea    (%rsi,%rdi,1),%eax   400477:   01 05 d3 03 20 00    add    %eax,0x2003d3(%rip)        # 600850 <accum>   40047d:   c3                   retq   40047e:   90                   nop   40047f:   90                   nop                
 与前面的代码对比差不多,但是其地址有明显的改变,应为第二段代码编译后说有的地址都是相对程序的地址指定了的。
访问信息 访问信息分为:操作数指示符、数据传送指令等操作。通过一个练习题来巩固一下操作数指示符:
例题3.1 假设下面的值存放在指明的存储器地址和寄存器中: 
 
地址 
值 
 
 
0x100 
0xFF 
 
0x104 
0xAB 
 
0x108 
0x13 
 
0x10C 
0x11 
 
 
寄存器 
值 
 
 
%eax 
0x100 
 
%ecx 
0x1 
 
%edx 
0x3 
 
 
填写下表,给出所示操作数的值。
操作数 
值 
思路 
 
 
%eax 
0x100 
直接从上面获取寄存器的初始值  
 
0x104 
0xAB 
从上面表中获取0x104地址的值  
 
$0x108 
0x108 
立即数  
 
(%eax) 
0xFF 
先获取寄存器%eax的值0x100,然后在获取地址0x100对应的值,间接寻址  
 
4(%eax) 
0xAB 
(基址+偏移量)寻址:(4 + 0x100) = (0x104) = 0xAB 
 
9(%eax, %edx) 
0x11 
变址寻址:(9 + %eax + %edx) = (0x10C) = 0x11 
 
260(%ecx, %edx) 
0x13 
变址寻址:(260 + %ecx + %edx) = (0x108) = 0x13 
 
0xFC(, %ecx, 4) 
0xFF 
比例变址寻址:(0xFC + 0 + %ecx * 4) = (0xFC+0x4) = (0x100)=0xFF 
 
(%eax, %edx, 4) 
0x11 
比例变址寻址:(%eax + %edx * 4)=(0x10C)=0x11 
 
 
栈操作说明:
 
通过书中图3.5可知,当push一个数值时,栈指针减小,向下移动;当pop一个数据时栈指针向上移动。一般用 %esp来存储栈指针的地址。
下面通过一个例子说明C语言中指针使用的方式,函数exchange代码如下:1 2 3 4 5 6 7 8 9 10 int exchange(int *xp, int y) {     int x = *xp;     *xp = y;     return x; } int a = 4; int b = exchange(&a, 3); printf("a = %d, b = %d\n", a, b); 
 这段代码会打印出:a = 3, b = 4 关键部分的汇编代码如下:1 2 3 4 5 # xp地址的值存储在8(%ebp), y的值存储在12(%ebp) movl 8(%ebp) %edx #获取xp地址的值,存储在%edx movl (%edx), %eax #获取xp地址所指向的值赋予变量x,函数结束时返回这个值 movl 12(%ebp), %ecx #获取y的值,存储在%ecx movl %ecx, (%edx) #%ecx的值存储在%edx所指向的值, 这时候*xp的值为y,xp地址的值没有变化 
 局部变量比如x,通常时保存在寄存器中。
算术和逻辑操作 加载有效地址 加载有效地址(load effective address) 指令leal实际上是movl指令的变形。通过下面一个例子来说明它的含义:1 2 3 # 假设%edx的值为x movl 7(%edx, %edx, 4), %eax #计算7 + x + 4*x = 5x +7 那么%eax的值就是地址5x+7地址处所存储的值. leal 7(%edx, %edx, 4), %eax #计算7 + x + 4*x = 5x +7 那么%eax的值就是地址的值5x+7,而不是这个地址存储的值. 
一元操作和二元操作 如果一个操作只有一个操作数,既是源又是目的,这个操作就是一元操作。 如果一个操作有两个操作数,第二个操作数既是源又是目的,这个操作就是二元操作。 如下:
指令 
效果 
操作 
 
 
INC D 
D <- D + 1 
一元操作 
 
ADD S, D 
D <- D + S 
二元操作 
 
 
移位操作 
类型 
操作 
命令 
示意图 
 
 
非循环移位 
逻辑左移/算术左移 
SHL/SAL 
 
 
非循环移位 
逻辑右移 
SHR 
 
 
非循环移位 
算术右移 
SAR 
 
 
循环移位 
不含进位位的循环左移 
ROL 
 
 
循环移位 
不含进位位的循环右移 
ROR 
 
 
循环移位 
含进位位的循环左移 
RCL 
 
 
循环移位 
含进位位的循环右移 
RCR 
 
 
 
控制 条件码 除了整数寄存器,CPU还维护着一组单个位的条件码(codition code) 寄存器,最常用的条件码有:  
CF :进位标志。最近的操作使最高位产生了进位。可以用来检测无符号操作数的溢出。 
ZF :零标志。最近的操作得出的结果为0。 
SF :符号标志。最近的操作得到的结果为负。 
OF :溢出标志。最近的操作导致一个补码溢出——正溢出或负溢出。 
 
访问条件码 条件码通常不会直接读取常用的使用方法有三种:
可以根据条件码的某个组合,将一个字节设置为0或者1; 
可以条件跳转到程序的某个其它部分; 
可以有条件地传送数据. 
 
对于第一种情况,举例说明一下:  
指令 
同义名 
效果 
设置条件 
 
 
sete D 
setz 
D<-ZF 
相等/零 
 
setne D 
setnz 
D<-~ZF 
不等/非零 
 
sets D 
 
D<-SF 
负数 
 
setns D 
 
D<-~SF 
非负数 
 
setg D 
setnle 
D<-~(SF ^ DF)&~ZF 
大于(有符号>) 
 
 
SET指令。每条指令根据条件码的某个组合将一个字节设置为0或者1,而不是直接访问条件码寄存器。
跳转指令及其编码 正常情况下,指令按照它们出现的顺序一条一条地执行。跳转(jump) 指令会导致执行切换到程序中一个全新的位置。在汇编代码中,这些跳转的目的地通常用一个标号(label) 指明。当执行与PC(程序计数器)相关的寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。例如一段汇编代码及其反汇编代码:1 2 3 4 5 6 7 8 9 10     jle .L2                         #if <=, goto dest2 .l5:                                #dest1:     movl    %edx, %eax           sar1    %eax     subl    %eax, %edx     leal    (%edx, %edx, 2), %edx     testl   %edx, %edx                   jg      .L5                     #if >, goto dest1 .L2:                                #dest2     movl    %edx, %eax 
1 2 3 4 5 6 7 8 8:   7e 0d        jle     17 <silly+0x17> a:   89 d0        mov     %edx, %eax c:   d1 f8        sar     %eax e:   29 c2        sub     %eax, %edx 10:  8d 14 52     lea     (%edx, %edx, 2), %edx 13:  85 d2        test    %edx, %edx 15:  7f f3        jg      a <silly+0xa> 17:  89 d0        mov     %edx, %eax 
 我们看到在反汇编代码里的第一行,前面地址为0x8的地方,0x7e代表jle, 0xd代表要跳转的地址,但是对应的汇编地址却是0x17, 其实0x17是通过0x8处的0xd与下一行0xa的地址(正是PC程序计数器的值)相加得出的。
翻译条件分支 如何将条件表达式和语句从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。通过一个例子来说明: a) 原始的C语言代码1 2 3 4 5 6 int absdiff(int x , int y) {     if (x < y)         return y - x;     else         return x - y; } 
 b) 与之等价的goto版本1 2 3 4 5 6 7 8 9 10 11 int absdiff(int x , int y) {     int result;     if (x >= y)         goto x_ge_y;     result = y - x;     goto done;     x_ge_y:         result = x - y;     done:         return result; } 
1 2 3 4 5 6 7 8 9 10 11 # x at %ebp + 8, y at %ebp + 12   movl 8(%ebp), %edx    #get x   movl 12(%ebp), %eax   #get y   cmpl %eax, %edx       #compare y:x 即用x-y的值更新标记位   jge  .L2              # x > y    subl %edx, %eax       # y = y - x   jmp  .L3              #goto L3 .L2:   subl %eax, %edx       #x = x - y   movl %edx, %eax       #y = x .L3:                    #done return x 
 
循环 其实循环和条件分支所利用的都是jump指令和标记寄存器的值,这里就不再说明了。
条件传送指令 实现条件操作的传统方法是利用控制 的条件转移。但是在现代处理器上,它可能会非常的低效率。数据 的条件转移是一种替代的策略。这种方法先计算一个条件操作的两种结果,然后再根据条件是否满足选取一个。通过一个例子来说明:1 2 3 int absdiff(int x , int y) {     return x < y ? y-x:x-y; } 
 产生的汇编代码:1 2 3 4 5 6 7 8 9 # x at %ebp + 8, y at %ebp + 12 movl    8(%ebp), %ecx   #get x movl    12(%ebp), %edx  #get y movl    %edx, %ebx      #copy y subl    %ecx, %ebx      #compute y - x movl    %ecx, %eax      #copy x subl    %edx, %eax      #compute x - y, set as return value cmpl    %edx, %ecx      #compare x:y cmovl   %ebx, %eax      #if <, replace return value with y - x 
 注意这个与上面的翻译条件分支 一节的汇编做比较。这种方式效率会更好。原因是跟第4章和第5章讲的处理器的结构有关系,处理器通过流水线(pipelining) 来获得高性能。第二种方式可以把所有的指令都放入到流水线中,而第一种方式则需要根据条件判断哪个指令放入到流水线,从这点可以看出第二种方式总是能把指令都放入流水线中,保证了执行的效率。
switch语句 switch(开关)语句可以根据一个整数索引值进行多重分支(multi-way branching)。处理具有多种可能结果的测试时。这种语句特别有用。它们不仅提高了C代码的可读性,而且通过使用跳转表(jump table) 这种数据结构使得实现更加高效。
过程 一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。数据传递、局部变量的分配和释放通过操纵程序栈来实现。
栈桢结构 为单个过程分配的那部分栈称为栈帧(stack frame) 。寄存器%ebp为帧指针 ,而寄存器%esp为栈指针 。栈帧结构(栈用来传递参数、存储返回信息、保存寄存器,以及本地存储)
转移控制 支持过程调用和返回的指令:
指令 
描述 
 
 
call Label 
过程调用 
 
call *Operand 
过程调用 
 
leave 
为返回准备栈 
 
ret 
从过程调用中返回 
 
 
call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令地址。 例如: 执行call的之后栈顶入栈的地址为call命令后那条命令的地址,然后ret执行后把这个地址弹出到%eip,开始执行%eip处的命令。%eip应该是程序计数器。还有一个要注意的是保存的%ebp ,这个起始是上一个帧的起始地址,当前过程执行完后,这个保存的%ebp就会返回到%ebp中,更新当前帧的开始地址为上一帧的地址。
寄存器使用惯例 程序寄存器组是唯一能被所有过程共享的资源。虽然在给定时刻只能有一个过程是活动的,但是我们必须保证当一个过程(调用者 )调用另一个过程(被调用者 )时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值。根据惯例,寄存器%eax、%edx和%ecx被划分为调用者保存 寄存器。当过程P调用Q时,Q可以覆盖这些寄存器,而不会破任何P所需要的数据。另一方面,寄存器%ebx、%esi和%edi被划分为被调用者保存 寄存器。这意味着Q必须在覆盖这些寄存器之前,先把它们保存到栈中,并在返回前恢复它们。
过程示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int swap_add(int *xp, int *yp) {     int x = *xp;     int y = *yp;          *xp = y;     *yp = x;     return x + y; } int caller() {     int arg1 = 534;     int arg2 = 1057;     int sum = swap_add(&arg1, &arg2);     int diff = arg1 - arg2;          return sum * diff; } 
 
1 2 3 4 5 6 7 8 9 10 11 12 caller:     pushl   %ebp    #save old %ebp, 保存上一个帧的开始地址     movl    %esp, %ebp  #set %ebp as frame pointer,更新%ebp地址为当前帧的地址     subl    $24, %esp   #alllocate 24 bytes on stack, 申请栈空间,指针头向下移动     movl    $534, -4(%ebp)  #set arg1 to 534     movl    $1057, -8(%ebp) #set arg2 to 1057     leal    -8(%ebp), %eax  #compute &arg2     movl    %eax, 4(%esp)   #store on stack     leal    -4(%ebp), %eax  #compute &arg1     movl    %eax, (%esp)    #store to stack     call    swap_add    #call the swap_add function 
ps:为什么GCC分配从不使用的空间 GCC为caller分配了24个字节,但是却只是用了16个字节,这是因为GCC坚持一个x86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍。包括%ebp值的4个字节和返回值的4个字节,caller一共使用了32个字节。采用这个规则是为了保证访问数据的严格对齐(alignment)。
 
swap_add开始代码:1 2 3 4 swap_add:     pushl   %ebp     movl    %esp, %ebp     pushl   %ebx #需要寄存器%ebp做为临时存储。因为这是一个被调用者保存的寄存器。 
 swap_add主体代码:1 2 3 4 5 6 7 movl    8(%ebp), %edx   #get xp movl    12(%ebp), %ecx  #get yp movl    (%edx), %ebx    #get x movl    (%ecx), %eax    #get y movl    %eax, (%edx)    #store y at xp movl    %ebx, (%ecx)    #store x at yp addl    %ebx, %eax      #return value = x+y 
 这段代码从caller的栈帧中取出它的参数,这点需要注意。 swap_add结束代码:1 2 3 popl    %ebx    #restore %ebx, 从栈帧中弹出之前存储的%ebx的值到%ebx,恢复%ebx的值 popl    %ebp    #restore %ebp, 从栈帧中弹出之前存储的%ebp的值到%ebp,恢复%ebp的值 ret             #return, 弹出地址到%eip, 并且从上个帧调用call下一行的地址开始执行。 
递归过程 上面描述的栈链接惯例使得过程能够递归地调用它们自身。因为每个调用过程都有它自己的私有空间,多个未完成调用的局部变量不会相互影响。
递归的阶乘程序的C代码:  
1 2 3 4 5 6 7 8 9 int rfact(int n) {     int result;     if (n <= 1)         result = 1;     else          result = n * rfact(n-1);     return result; } 
 
 
对应的汇编代码
 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # argument: n at %ebp + 8 # registes: n in %ebx, result in %eax rfact:     pushl   %ebp    #save old %ebp     movl    %esp, %ebp  #set %ebp as frame pointer     pushl   %ebx    #save callee save register %ebx     subl    $4, %esp    #allocate 4 byte on stack     movl    8(%ebp), %ebx   #get n     movl    $1, %eax #result = 1     cmpl    $1, $ebx    # 比较参数 n 与 1     jle     .L53    #if n <= 1, 跳到L53     leal    -1(%ebx), %eax  #%eax <- n-1     movl    %eax, (%esp) #n-1保存到栈顶     call    rfact   #调用rfact(n-1)     imull   %ebx, $eax #result = return value * n .L53:     addl    $4, %esp     popl    %ebx     popl    %ebp     ret 
 
当n=1时,调用L53,被调用过程返回,然后继续执行imull指令 %eax 值 1
 递归的阶乘函数的栈帧,这是递归调用之前的帧状态。
数组分配和访问 C语言中数组是一种将标量数据聚集成为更大数据类型的方式。
基本原则 对于数据类型T和整型常数N,声明如下: T A[N] 它有两个效果:  
它在存储器中分配一个\( L \bullet N \)字节的连续区域;这里L是数据类型T的大小(单位为字节), 用 \( x_{A} \)   来表示起始位置。   
它引入了标符A;可以用A作为指向数组开头的指针,这个指针的值就是 \(x_{A}\)  。数组元素i被存放在地址为 \(x_{A} + L \bullet i\)的地方。 通过例子加深理解:1 2 3 4 char    A[12]; char    *B[8]; //指针数组,数组的值是(char *)类型,指针的地址是int型的。 double  C[6]; double  *D[5]; 
 
 
 
数组 
元素大小 
总的大小 
起始地址 
元素i 
 
 
A 
1 
12 
\(x_{A}\) 
\(x_{A} + i\) 
 
B 
4 
32 
\(x_{B}\) 
\(x_{B} + 4i\) 
 
C 
8 
48 
\(x_{C}\) 
\(x_{C} + 8i\) 
 
D 
4 
20 
\(x_{D}\) 
\(x_{D} + 4i\) 
 
 
异质的数据结构 C语言提供了两种结合不同类型的对象来创建数据类型的机制:结构(structure) ,用关键字struct声明, 将多个对象集合到一个单位中;联合(union) , 用关键字union声明,允许用集中不同的类型来引用一个对象。
数据对齐 许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值k(通常是2,4或8)的倍数。这种对齐限制 简化了形成处理器和存储器系统之间接口的硬件设计。 例如:1 2 3 4 5 struct S1 {     int i;     char c;     int j; }; 
 按照最小字节分配应该是这样的: 但是要遵循字节对齐,真实的分配情况是这样的:   
综合:理解指针 指针和它们映射到机器代码的关键原则:  
每个指针都对应一个类型。void * 类型代表通用指针。 
每个指针都有一个值。这个值是某个指定类型对象的地址。 
指针用&运算符创建。常用leal指令来计算表达式的值。int * p = &x 
操作符用于指针的间接引用。 
数组与指针紧密联系。 
将指针从一种类型强制转换为另一种类型,只改变它的类型,而不改变它的值。 
指针也可以指向函数。 
 
存储器的越界引用和缓冲区溢出 C对于数组引用不进行任何边界检查,而且局部变量和状态信息(例如保存的寄存器值和返回地址),都存放在栈中。这两种情况结合到一起就可能导致严重的程序错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息。一种常见的状态破坏称为缓冲区溢出(buffer overflow) 。入下图所示: 为buf分配的空间只有8个字节元素,上面地址保存的是%ebx, %ebp和返回地址的值,如果buf的长度超过分配的长度,就会把多出来的元素写入到保存这些寄存器地址的值,破坏%ebx,%ebp和返回地址的值,这样程序就不能正确的返回%ebx的值,不能正确的返回上以栈帧的地址,不能正确返回下一条要执行的命令地址。如果这些输入的元素包含一些可执行代码的字节编码,称为攻击代码(exploit code) , 比如串改返回地址的值,使程序返回到恶意程序的代码地址进行执行,就会成为我们所说的蠕虫和病毒 。 对抗缓冲区溢出攻击的方法:(详细的原理不再赘述,可以自己google)  
栈随机化。 
栈破坏检测。 
限制可执行代码区域。