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.指针的本质

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

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

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

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

  2. 六星经典CSAPP笔记(1)计算机系统巡游

    CSAPP即<Computer System: A Programmer Perspective>的简称,中文名为<深入理解计算机系统>.相信很多程序员都拜读过,之前买的旧版没 ...

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

    2.Representing and Manipulating Information 本章从二进制.字长.字节序,一直讲到布尔代数.位运算,最后无符号.有符号整数.浮点数的表示和运算.诚然有些地方的 ...

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

    六星经典CSAPP-笔记(7)加载与链接 1.对象文件(Object File) 1.1 文件类型 对象文件有三种形式: 可重定位对象文件(Relocatable object file):包含二进制 ...

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

    六星经典CSAPP-笔记(11)网络编程 参照<深入理解计算机系统>简单学习了下Unix/Linux的网络编程基础知识,进一步深入学习Linux网络编程和TCP/IP协议还得参考Steve ...

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

    六星经典CSAPP-笔记(12)并发编程(上) 1.并发(Concurrency) 我们经常在不知不觉间就说到或使用并发,但从未深入思考并发.我们经常能"遇见"并发,因为并发不仅仅 ...

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

    六星经典CSAPP-笔记(10)系统I/O 1.Unix I/O 所有语言的运行时系统都提供了高抽象层次的I/O操作函数.例如,ANSI C在标准I/O库中提供了诸如printf和scanf等I/O缓 ...

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

    CSAPP:程序的机器级表示2 关键点:算术.逻辑操作 算术逻辑操作1.加载有效地址2.一元二元操作3.移位操作 算术逻辑操作   如图列出了x86-64的一些整数和逻辑操作,大多数操作分成了指令类( ...

  9. CSAPP:第三章程序的机器级表示1

    CSAPP:程序的机器级表示1 关键点:数据格式.操作数指示符. 数据格式访问信息操作数指示符举例说明 数据格式   术语字(word)表示16位数据类型,32位数为双字(double words), ...

随机推荐

  1. [LeetCode] Max Consecutive Ones II 最大连续1的个数之二

    Given a binary array, find the maximum number of consecutive 1s in this array if you can flip at mos ...

  2. codeforces 809E Surprise me!

    Tired of boring dates, Leha and Noora decided to play a game. Leha found a tree with n vertices numb ...

  3. Trie模版

    struct Trie{ Trie* nxt[]; int v; Trie(){ ;i<;i++){ nxt[i]=NULL; } v=-; } void insert(char s[],int ...

  4. 计蒜客NOIP2017提高组模拟赛(三)day2-直线的交点

    传送门 简单几何+逆序对 发现当两条直线甲乙与平板的交点在上面甲在较左的位置,那么下面甲在较右的位置就可以相交 然后把上面的位置排下序,下面离散化+树状数组即可 #include<cstdio& ...

  5. C++Primer学习——const

    Const int size = 512; 在编译的时候,编译器会把用到该变量的地方全部替换成对应的值. const&可以绑定字面值,所以当用常量引用绑定一个常量时,是否可以看成那个值在编译阶 ...

  6. poj 1367 robot(搜索)

    题意:给你一个图,求起点 到 终点的最少时间 每次有两种选择①:往前走1~3步                ②原地选择90°   费时皆是1s 图中1为障碍物,而且不能出边界.还要考虑机器人的直径 ...

  7. lombok安装与简易教程(一)

    lombok简单的来讲就是在编译的时候,可以帮助我们生成getter与setter等方法,减少代码量.这可是一个好东西啊 1.eclipse安装lombok java -jar lombok.jar ...

  8. 使用jquery.qrcode.js生成二维码

    通常生成二维码的方式有两种:第一种是java代码的形式,第二种是通过Js方式. 在这里我做个记录,用js生成二维码,可以在官网下载源码:http://jeromeetienne.github.io/j ...

  9. day4 liaoxuefeng---高级特性

    掌握了Python的数据类型.语句和函数,基本上就可以编写出很多有用的程序了. 但是在Python中,代码不是越多越好,而是越少越好.代码不是越复杂越好,而是越简单越好. 基于这一思想,我们来介绍Py ...

  10. P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解

    1.内容概述 P2P即点对点通信,或称为对等联网,与传统的服务器客户端模式(如下图"P2P结构模型"所示)有着明显的区别,在即时通讯方案中应用广泛(比如IM应用中的实时音视频通信. ...