六星经典CSAPP-笔记(3)程序的机器级表示

1.前言


IA32机器码以及汇编代码都与原始的C代码有很大不同,因为一些状态对于C程序员来说是隐藏的。例如包含下一条要执行代码的内存位置的程序指针(program counter or PC)以及8个寄存器。还要注意的一点是:汇编代码的ATT格式Intel格式。ATT格式是GCC和objdump等工具的默认格式,在CSAPP中一律使用这种格式。而Intel格式则通常会在Intel的IA32架构文档以及微软的Windows技术文档中碰到。两者的主要区别有:

  • Intel格式忽略指令中暗示操作数长度的后缀,例如mov而不是ATT格式的movl
  • Intel格式忽略寄存器名称前的%,例如esp而不是ATT格式的%esp
  • Intel格式用不同的方式描述内存位置,例如DWORD PTR [ebp+8]而不是ATT格式的8(%ebp)
  • Intel格式指令的操作数顺序与ATT格式的完全相反,ATT格式总是最后一个操作数是目标,例如movl %eax, (%edx)。

此外,作为16位处理器架构的遗留产物,如今的指令依旧用word指2个字节16位,而用double word指4个字节。所以指令中通常使用B、W、L表示操作数是1、2、4个字节的指令,例如数据移动指令的三个版本movb、movw、movl。

这一章通过学习程序的机器级底层表示,学会阅读底层代码。为什么逆向工程很难?因为源代码与编译后的代码往往不是一一对应的。编译器会引入源代码中不存在的新变量,同时为了节约寄存器的使用,编译器也经常将多个值映射到一个寄存器。对于循环来说,通过观察寄存器是如何在循环前初始化,在循环内的更新和条件检测以及循环后的使用,能够得到一些线索。

2.寄存器与寻址


第一章的笔记中我们看到,程序执行的很大一部分时间都是在将数据挪来挪去的。所以处理器支持只使用寄存器的1、2、4个字节,同时并且支持多种寻址方式。如下图右半边的表格中所示,这样我们就可以灵活地从内存中加载数据到寄存器,或者将寄存器中的值保存到内存。

虽然看起来有些眼花缭乱,但实际上最基本的形式就是最后一种:Imm(Eb, Ei, s)=Imm+R[Eb]+R[Ei]*s (R[X]指寄存器X的值)。一共四个参数控制寻址,看起来有些过于灵活,那就让我们想象一下它的应用场景。先不考虑Imm,那么最典型的应用就是访问数组中某个数据项。假如数组为int x[4],则此时Eb就是数组的首地址,相当于x,而Ei就是要访问数据项的下标,而s就是数组中数据类型的长度。例如我们要访问x[3],那么就相当于(x, 3, sizeof(int))=x+3*4。用C语言来写就是*(x+3),因为C语言自动按照指针的类型长度进行移动(编译器自动生成正确的代码),所以我们并不用自己计算偏移量乘以sizeof(int),但这都是后话了。那再加上Imm又能有何种应用场景,其实很简单,就是访问struct中数组中某一项。如下图所示,直接一条指令就能访问到结构中的数组中的某一项。

3.常用指令


下面是一些最常见的汇编指令及其含义:

  • mov:数据移动。IA32强加了一条限制:一条移动指令的两个操作数不能都是内存地址。所以从一个内存位置拷贝数据到另一个内存位置是需要两条指令的。
  • leal:加载地址。效果就是mov Imm(%a, %b, s), %x会将%x赋值为Imm+%a+s*%b,而不是M[Imm+%a+s*%b],所以有两个很有用的场景:1)拷贝地址。例如int *x=a汇编为mov (%eax), %edx,那么int x=&a汇编为leal (%eax), %edx。所以leal不会真的将a的值(即(%eax))保存到x(即%edx),而只是将a的地址(其实就是%eax)保存到x。2)简单算术运算。第二个很自然会想到的应用就是使用leal一条指令压缩简单的算术运算,例如leal 7(%edx, %edx, 4)=5x+7。
  • jmp:直接跳转到标签,或间接跳转到寄存器中指定的地址。对于直接跳转,在汇编语言中通常就是符号化的标签表示。但之后汇编器或链接器要对其进行编码,最常见的编码方式就是PC相对地址。即用1、2、4字节的偏移量表示跳转目标地址与jmp指令紧接着的下一条指令的地址,如下图所示。但为什么是紧接着jmp指令的下一条指令的地址而不是jmp这一条的?其实也是有历史原因的,因为早期的处理器实现是先更新PC计数器作为第一步,然后再执行当前指令的。所以指令在执行的时候,其实PC已经指向下一条指令了,因此跳转的偏移量也就要相对下一条指令来说了。

4.类型转换时发生了什么


有符号转成无符号整数时,我们期望着编译器能将负数变成0,正数保留不变,长过最大长度的正数赋值成TMax。然而实际上相同长度的整数转换其实只是简单拷贝,什么都不做。并且当同时需要长度转换和类型转换时,C语言首先进行长度转换。长度转换后两个整数就都变成相同长度了,所以我们只需关注不同长度整数间的扩展和截断是如何进行的:

  • 扩展:无符号进行零扩展,即用零填充高位。有符号进行符号扩展,即用最高位-符号位填充高位。
  • 截断:简单地扔掉高位字节。对于小尾端来说,就是反过来,拷贝寄存器的高位如%al。

因为有符号整数在大部分机器上都是用反码进行编码的,对反码进行有符号扩展是不会改变其值的,在第二章中有过证明。反码就是这样神奇!0有唯一表示,并且有符号扩展时值还不变!关键就在于:高位扩展出一个1后,-2w+2w-1=-2w-1,还是等于扩展前的原值。

5.逻辑运算为什么要短路


第二章笔记中曾说过位运算和逻辑运算的两个区别,一是逻辑运算的眼中只有TRUE和FALSE,非0的不管是几都会被看做TRUE。而第二个区别就是逻辑运算的短路效果。那为什么逻辑运算会短路?因为逻辑运算是用jmp实现的。在汇编语言中,逐一判断条件表达式中的各个部分的真假,当某一部分判断出结果就直接跳转了。正因为逻辑运算是决定朝哪里运行,而不像位运算得出一个最终结果,所以汇编语言可以用跳转实现,所以就产生了高级语言中短路的性质

6.局部变量其实就在寄存器里


其实局部变量是直接存储在寄存器的,大部分情况下都会一直在寄存器中,而不会落地到内存。例如第7部分中的函数swap_add(),函数运行时栈帧(内存)实际上没有保存任何局部变量。整个函数的局部变量和逻辑都在寄存器和ALU中执行完成。

在以下情况,局部变量会被保存在内存中(栈上):

  • 当没有足够的寄存器来保存所有局部变量时。毕竟寄存器只有八个。
  • 一些局部变量是数组或struct,因此必须通过指针访问。
  • 当对局部变量进行取地址&运算时,因此必须产生一个内存地址给它。

7.运行时的代码与栈


下面来看一个函数调用的例子,深入学习代码底层是如何运行的。

caller()代码如下:

swap_add()代码如下:

编译器生成的代码会遵守一定的规则,这样在执行各种跳转、函数调用时才不会发生数据覆盖等问题,从而使程序正确的运行。

8.指针的本质


也许之前也曾听过,指针本质上就是一个内存地址。但之前没有顿悟,现在通过研究底层知识来强化理解。从下图可以看出,指针取值实际上是一种很自然的操作,因为大多数时候我们没法在一个寄存器里放下一个变量表示的全部数据,例如数组或结构。如果寄存器能够放下整个数组和结构,那我们当然没必要用指针了。所以很自然地,我们就会先加载数据的首地址的内存地址(就是指针!)到寄存器,然后再去访问寄存器指向的内存位置。

时间: 2024-11-03 05:30:37

六星经典CSAPP-笔记(3)程序的机器级表示的相关文章

六星经典CSAPP笔记系列 - 作者:西代零零发

六星经典CSAPP笔记(1)计算机系统巡游 六星经典CSAPP笔记(2)信息的操作和表示 六星经典CSAPP-笔记(3)程序的机器级表示

六星经典CSAPP笔记(2)信息的操作和表示

2.Representing and Manipulating Information 本章从二进制.字长.字节序,一直讲到布尔代数.位运算,最后无符号.有符号整数.浮点数的表示和运算.诚然有些地方的数学证明有些枯燥,但总体上看,本章还是干货十足的! 2.1 Decimal vs. Binary Notation 我们习惯十进制只是因为我们有十根手指头(?),所以会对二进制感到不习惯.但是二值信号(two-value signal)在表示.存储.传输方面有巨大优势,从打孔带上的有没有孔洞(代码的

六星经典CSAPP-笔记(7)加载与链接(上)

六星经典CSAPP-笔记(7)加载与链接 1.对象文件(Object File) 1.1 文件类型 对象文件有三种形式: 可重定位对象文件(Relocatable object file):包含二进制代码和数据,能与其他可重定位对象文件在编译时合并创建出一个可执行文件. 可执行对象文件(Executable object file):包含可以直接拷贝进行内存执行的二进制代码和数据. 共享对象文件(Shared object file):一种特殊的可重定位对象文件,能在加载时或运行时,装载进内存进

六星经典CSAPP-笔记(11)网络编程

六星经典CSAPP-笔记(11)网络编程 参照<深入理解计算机系统>简单学习了下Unix/Linux的网络编程基础知识,进一步深入学习Linux网络编程和TCP/IP协议还得参考Stevens的书. 1.网络基础 (略过,待补充) 2.IP地址 2.1 IP地址的表示 IP地址是一个无符号的32位整数.Linux网络程序使用下面这种IP地址结构存储IP地址: /* Internet address structure */ struct in_addr { unsigned int s_add

六星经典CSAPP-笔记(10)系统IO

六星经典CSAPP-笔记(10)系统I/O 1.Unix I/O 所有语言的运行时系统都提供了高抽象层次的I/O操作函数.例如,ANSI C在标准I/O库中提供了诸如printf和scanf等I/O缓冲功能的函数:C++中则重载了<<和>>用来支持读写.在Unix系统中,这些高层次的函数基于Unix的系统I/O函数来实现,多数时候我们都无需直接使用底层的Unix I/O.但学习Unix系统I/O能更好地理解一些系统概念,而且当高层次的函数不适用时我们也能轻松地实现想要的功能,例如访

六星经典CSAPP-笔记(12)并发编程(上)

六星经典CSAPP-笔记(12)并发编程(上) 1.并发(Concurrency) 我们经常在不知不觉间就说到或使用并发,但从未深入思考并发.我们经常能"遇见"并发,因为并发不仅仅是操作系统内核的"绝招",它也是应用开发中必不可少的技巧: 访问慢I/O设备:就像当应用程序等待I/O中的数据时内核会切换运行其他进程一样,我们的应用也可以用类似的方式,将I/O请求与其他工作重叠从而挖掘并发的潜能. 推迟工作而减少延迟:我们可以推迟一些耗时工作稍后执行,例如内存分配器不在

深入理解计算机系统之程序的机器级表示部分学习笔记

不论我们是在用C语言还是用JAVA或是其他的语言编程时,我们会被屏蔽了程序的机器级的实现.机器语言不需要被编译,可以直接被CPU执行,其执行速度十分  快.但是机器语言的读写性与移植性较高级语言低.高级语言被编译后便成为了汇编语言,汇编语言十分接近机器语言.之后汇编代码会转化为机器语言.虽然现代  的编译器能帮助我们将高级语言转化为汇编语言,解决了不少问题,但是对于一个严谨的程序员来说,需要做到能够阅读和理解汇编语言.我们主要围绕Intel来讲  解. 一  Intel处理器的历史演变 Inte

CSAPP:第三章程序的机器级表示2

CSAPP:程序的机器级表示2 关键点:算术.逻辑操作 算术逻辑操作1.加载有效地址2.一元二元操作3.移位操作 算术逻辑操作 ??如图列出了x86-64的一些整数和逻辑操作,大多数操作分成了指令类(只有leaq没有其他的变种,addb.addw.addl.addq分别是字节加法.字加法.双字加法和四字加法),这些操作通常分为四组:加载有效地址.一元操作.二元操作和移位操作. 1.加载有效地址 leaq S,D;D = &S??加载有效地址指令leag实际上是movq指令的变形,它的指令形式上是

第三章 程序的机器级表示

程序的机器级表示 3.1历史观点 8086—〉80286—〉i386—〉i486—〉Pentium—〉PentiumPro—〉Pentium—〉Pentium—〉Pentium4—〉Pentium4e—〉Core 2 Duo —〉Core i7 3.2程序编码 1.gcc -01 –o p p1.c p2.c      使用第一级优化 2.程序计数器(%eip)指示将要执行的下一条指令在存储器中的地址. 3.寄存器文件 4.-S:C语言编译器产生的汇编代码 例:gcc -01 –S code.c