目录
记录下一些收获
第二章 信息的表示和处理
- 补码概念的理解:
我们知道,补码的表示中,最高位为1表示该数为负,为0表示该数为正。
之前一直死记硬背,要求一个负数的十进制大小,就采取求补码操作的逆过程,比如求1010,先减1得1001,再将符号位不变,其余位取反得1110,即-6。
在本书上看到的概念是:将字的最高位解释为负权。
例如求1010的十进制值,可以这样求:-8 + 0 + 2 + 0 = -6。这样较好理解,如下
- 浮点数
浮点数的表示、运算等
这段还得好好看看,可见这篇博客https://www.cnblogs.com/mikewolf2002/p/10095995.html
第三章 程序的机器级表示
寄存器
通常,一个X86-64 的中央处理单元(CPU)包含一组16 个存储64 位值的通用目的寄存器。这些寄存器用来存储整数数据和指针。展示如下:
给出一个例子,说明程序中是如何使用寄存器进行数据传送的
设有函数如下:
long exchange(long *xp, long y)
{
long x = *xp;
*xp = y;
return x;
}
其汇编代码为
long exchange(long *xp, long y)
xp in %rdi, y in %rai;
1 exchange:
2 movq (%rdi), %rax Get x at xp, Set as return value
3 movq %rsi, (%rdi) Store y as xp
4 ret Return
- 算术与逻辑操作
X86-64的一些整数和逻辑操作列举如下
这些指令类有各种带不同大小的操作数的变种(只有leaq没有其他大小的变种)。例如,ADD由四条加法指令组成:addb、addw、addl、addq,分别是字节加法、字加法、双字加法和四字加法。事实上,给出的每个指令类都有对这四种不同大小数据的指令。这些操作被分为四组:加载有效地址、一元操作、二元操作和移位。二元操作有两个操作数,而一元操作有一个操作数。
此处特别说明一下加载有效地址leaq的用法:
加载有效地址(load effective address)指令leaq 实际上是movq 指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。上图中我们用C 语言的地址操作符&S 说明这种计算。这条指令可以为后面的内存引用产生指针。另外,它还可以简洁地描述普通的算术操作。例如,如果寄存器%rdx的值为X,那么指令leaq 7(%rdx,%rdx,4),%rax 将设置寄存器%rax 的值为5x+7。编译器经常发现leaq的一些灵活用法,根本就与有效地址计算无关。目的操作数必须是一个寄存器。
举例说明:现有程序如下
long scale(long x,long y,long z)
{
long t = x+4*y+12*z;
return t;
}
编译时,该函数的算术运算以三条leaq指令实现:
long scale(long x, long y ,long z)
x in %rdi, y in % rsi, z in %rdx
scale:
leaq (%rdi,%rsi,4), %rax x + 4*y
leaq (%rdx,%rdx,2), %rdx =z+2*z = 3*z
leaq (%rax,%rdx,4), %rax =(x+4*y) + 4*(3*z) = x + 4*y
ret
栈
- 过程
过程是软件中一种很重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。设计良好的软件用过程作为抽象机制,隐藏某个行为的具体实现,同时又提供清晰简洁的接口定义,说明要计算的是哪些值,过程会对程序状态产生什么样的影响。不同编程语言中,过程的形式多样:函数(function) 、方法(method)、子例程(subroutine)、处理函数(handler)等等,但是它们有一些共有的特性。要提供对过程的机器级支持,必须要处理许多不同的属性。为了讨论方便,假设过程P 调用过程Q, Q 执行后返回到P,这些动作包括下面一个或多个机制:
传递控制。在进入过程Q 的时候,程序计数器必须被设置为Q 的代码的起始地址,然后在返回时,要把程序计数器pc设置为P 中调用Q 后面那条指令的地址。
传递数据。P 必须能够向Q 提供一个或多个参数,Q 必须能够向P 返回一个值。
分配和释放内存。在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些存储空间。
- 运行时栈
C语言过程调用机制的一个关键特性(大多数其他语言也是如此)在于使用了栈数据结构提供的后进先出的内存管理原则。当X86-64 过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧(stack fram)。
当前正在执行的过程的帧总是在栈顶。当过程P 调用过程Q 时,会把返回地址压入栈中,指明当Q 返回时,要从P 程序的哪
个位置继续执行。我们把这个返回地址当做P 的栈帧的一部分,因为它存放的是与P 相关的状态。Q 的代码会扩展当前栈的边界,分配它的找帧所需的空间。在这个空间中,它可以保存寄存器的值,分配局部变量空间,为它调用的过程设置参数。大多数过程的栈帧都是定长的,在过程的开始就分配好了。但是有些过程需要变长的帧,这个问题会在3.10.5节中讨论。通过寄存器,过程P 可以传递最多6 个整数值(也就是指针和整数), 但是如果Q 需要更多的参数,P 可以在调用Q 之前在自己的栈帧里存储好这些参数。
将控制从函数P 转移到函数Q 只需要简单地把程序计数器(PC)设置为Q 的代码的起始位置。不过,当稍后从Q 返回的时候,处理器必须记录好它需要继续P 的执行的代码位置。在x86-64 机器中,这个信息是用指令call Q 调用过程Q 来记录的。该指令会把地址A 压入栈中,并将PC 设置为Q 的起始地址。压人的地址A 被称为返回地址,是紧跟在call 指令后面的那条指令的地址。对应的指令ret 会从栈中弹出地址A,并把PC 设置为A。
- 内存越界引用与缓冲区溢出
C 对于数组引用不进行任何边界检查,而且局部变量和状态信息(例如保存的寄存器值和返回地址)都存放在栈中。这两种情况结合到一起就能导致严重的程序错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息。当程序使用这个被破坏的状态,试图重新加载寄存器或执行ret 指令时,就会出现很严重的错误。一种特别常见的状态破坏称为缓冲区溢出(buffer overflow),通常,在栈中分配某个字符数组来保存一个字符串,但是字符串的长度超出了为数组分配的空间。如果存储的返回地址的值被破坏了,那么ret 指令会导致程序跳转到一个完全意想不到的位置。
缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。这是一种最常见的通过计算机网络攻击系统安全的方法。通常,输人给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击代码(exploit code), 另外,还有一些字节会用一个指向攻击代码的指针覆盖返回地址。那么,执行ret 指令的效果就是跳转到攻击代码。
在一种攻击形式中,攻击代码会使用系统调用启动一个shell 程序,给攻击者提供一组操作系统函数。在另一种攻击形式中,攻击代码会执行一些未授权的任务,修复对栈的破坏,然后第二次执行ret 指令,(表面上)正常返回到调用者。让我们来看一个例子,在1988 年11 月,著名的Internet 蠕虫病毒通过Internet 以四种不同的方法获取对许多计算机的访问。一种是对finger 守护进程fingerd 的缓冲区溢出攻击,fingerd 服务FINGER 命令请求。通过以一个适当的字符串调用FINGERÿ 蠕虫可以使远程的守护进程缓冲区溢出并执行一段代码,让蠕虫访问远程系统。一旦蠕虫获得了对系统的访问,它就能自我复制,几乎完全地消耗掉机器上所有的计算资源。结果,在安全专家制定出如何消除这种蠕虫的方法之前,成百上千的机器实际上都瘫痪了。这种蠕虫的始作俑者最后被抓住并被起诉。时至今日,人们还是不断地发现遭受缓冲区溢出攻击的系统安全漏洞,这更加突显了仔细编写程序的必要性。任何到外部环境的接口都应该是“防弹的”,这样,外部代理的行为才不会导致系统出现错误。
- 对抗缓冲区溢出攻击
缓冲区溢出攻击的普遍发生给计算机系统造成了许多的麻烦。现代的编译器和操作系统实现了很多机制,以避免遭受这样的攻击,限制人侵者通过缓冲区溢出攻击获得系统控制的方式。在本节中,我们会介绍一些Linux 上最新GCC 版本所提供的机制。
1.栈随机化
为了在系统中插入攻击代码,攻击者既要插入代码,也要插人指向这段代码的指针,这个指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。在过去,程序的栈地址非常容易预测。对于所有运行同样程序和操作系统版本的系统来说,在不同的机器之间,栈的位置是相当固定的。因此,如果攻击者可以确定一个常见的Web服务器所使用的栈空间,就可以设计一个在许多机器上都能实施的攻击,以传染病来打个比方,许多系统都容易受到同一种病毒的攻击,这种现象常被称作安全单一化(security monoculture )。
栈随机化的思想使得栈的位置在程序每次运行时都有变化。因此,即使许多机器都运行同样的代码,它们的栈地址都是不同的。实现的方式是:程序开始时,在栈上分配一段0~n字节之间的随机大小的空间,例如,使用分配函数alloca 在栈上分配指定字节数量的空间。程序不使用这段空间,但是它会导致程序每次执行时后续的栈位置发生了变化。分配的范围n必须足够大,才能获得足够多的栈地址变化,但是又要足够小,不至于浪费程序太多的空间。
在Linux 系统中,栈随机化已经变成了标准行为。它是更大的一类技术中的一种,这类技术称为地址空间布局随机化(Address-Space Layout Randomization), 或者简称ASIJR。采用ASLR, 每次运行时程序的不同部分,包括程序代码、库代码、栈、全局变量和堆数据,都会被加载到内存的不同区域。这就意味着在一台机器上运行一个程序,与在其他机器上运行同样的程序,它们的地址映射大相径庭。这样才能够对抗一些形式的攻击。
然而,一个执著的攻击者总是能够用蛮力克服随机化,他可以反复地用不同的地址进行攻击。一种常见的把戏就是在实际的攻击代码前插入很长一段的nop (读作“no op”,no operation 的缩写)指令。执行这种指令除了对程序计数器加一,使之指向下一条指令之外,没有任何的效果。只要攻击者能够猜中这段序列中的某个地址,程序就会经过这个序列,到达攻击代码。这个序列常用的术语是“空操作雪橇(nop sled)”,意思是程序会“滑过”这个序列。如果我们建立一个256 个字节的nop sled,那么枚举2^15=32 768 个起始地址,就能破解n=2^23的随机化,这对于一个顽固的攻击者来说,是完全可行的。对于64 位的情况,要尝试枚举2^24=16777216就有点儿令人畏惧了。我们可以看到栈随机化和其他一些ASLR 技术能够增加成功攻击一个系统的难度,因而大大降低了病毒或者蠕虫的传播速度,但是也不能提供完全的安全保障。
2.栈破坏检测
计算机的第二道防线是能够检测到何时栈已经被破坏。我们在echo 函数示例(图3-40)中看到,破坏通常发生在当超越局部缓冲区的边界时。在C 语言中,没有可靠的方法来防止对数组的越界写,但是,我们能够在发生了越界写的时候,在造成任何有害结果之前,尝试检测到它。
最近的GCC 版本在产生的代码中加入了一种栈保护者( (stack protector)机制,来检测缓冲区越界。其思想是在栈帧中任
何局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值,如图3-42 所示。这个金丝雀值,也称为哨兵值(guard value), 是在程序每次运行时随机产生的,因此,攻击者没有简单的办法能够知道它是什么。在恢复寄存器状态和从函数返回之前,程序检査这个金丝雀值是否被该函数的某个操作或者该函数调用的某个函数的某个操作改变了。如果是的,那么程序异常中止。
最近的GCC 版本会试着确定一个函数是否容易遭受栈溢出攻击,并且自动插入这种溢出检测。实际上,对于前面的栈溢出展示,我们不得不用命令行选项“-fno-stack-protector”来阻止GCC 产生这种代码。
3.限制可执行代码区域
最后一招是消除攻击者向系统中插人可执行代码的能力。一种方法是限制哪些内存区域能够存放可执行代码。在典型的程序中,只有保存编译器产生的代码的那部分内存才需要是可执行的。其他部分可以被限制为只允许读和写。正如第9 章中会看到的,虚拟内存空间在逻辑上被分成了页(page),典型的每页是2048 或者4096 个字节。硬件支持多种形式的内存保护,能够指明用户程序和操作系统内核所允许的访问形式。许多系统允许控制三种访问形式:读(从内存读数据)、写(存储数据到内存)和执行(将内存的内容看作机器级代码)。以前,x86 体系结构将读和执行访问控制合并成一个1 位的标志,这样任何被标记为可读的页也都是可执行的。栈必须是既可读又可写的,因而栈上的字节也都是可执行的。已经实现的很多机制,能够限制一些页是可读但是不可执行的,然而这些机制通常会带来严重的性能损失。
最近,AMD 为它的64 位处理器的内存保护引人了“NX”(No-Execme,不执行)位,将读和执行访问模式分开,Intel 也跟进了。有了这个特性,栈可以被标记为可读和可写,但是不可执行,而检査页是否可执行由硬件来完成,效率上没有损失。
第五章 程序性能优化
性能分析工具
gprof
Unix 系统提供了一个剖析程序GPROF ,这个程序产生两种形式的信息。首先,它确定程序中每个函数花费了多少CPU 时间。其次,它计算每个函数被调用的次数,以执行调用的函数来分类。这两种形式的信息都非常有用。这些计时给出了不同函数在确定整体运行时间中的相对重要性。调用信息使得我们能理解程序的动态行为。通过分析应用程序运行时产生的“flat profile”,可以得到每个函数的调用次数,每个函数消耗的处理器时间,也可以得到函数的“调用关系图”,包括函数调用的层次关系,每个函数调用花费了多少时间。
使用步骤
1) 用gcc、g++、xlC编译程序时,使用-pg参数,如:g++ -pg -o test.exe test.cpp
编译器会自动在目标代码中