汇编语言第一讲:计算机的组织架构和汇编语言介绍

  • 第一讲:计算机的组织架构和汇编语言介绍
    • 汇编语言
    • 计算机组织架构
      • 数字电路
      • 术语回顾
      • 数制
    • 数字电路
      • 硬件电路
      • 数字电路的问题
    • 汇编语言的开始
      • 程序的节(sections)
      • 调用操作系统的系统调用
      • 列出文件(Listing files)
      • 汇编和链接
      • 调试汇编程序
      • 反汇编现有的程序
    • 附录
      • 课程资源

第一讲:计算机的组织架构和汇编语言介绍

汇编语言

汇编语言和C/C++语言的区别是什么?

  • 汇编语言是底层语言,更接近于CPU本身可以理解的内容。CPU可以理解的是纯粹的字节流(机器语言)。几乎不会有人愿意通过写原始的字节流进行编程。汇编语言处于机器语言的上层, 将CPU可以理解的操作码(Opcode)抽象成了人类可以理解的命令, 例如add,mov等等。这些名字被称之为助记符

  • 和C/C++语言相比,汇编语言的工具较少。没有所谓的"标准汇编语言库"。如果你想要写一个字符串处理的方法,你只能自己编写。

  • 汇编语言不能移植到其他类型的CPU上(x86 vs ARM)或者其他类型的操作系统上(Windows vs Linux), 甚至不能和其它类型的汇编语言(YASM vs MASM vs GAS)都无法兼容。

一般来说, 我们编译一个C/C++ 程序的流程如下所示:

                    compile                 link
C/C++ source code    -->      object code    -->    executable 

其中object code是指指令流, 这些指令将会被CPU直接运行。C/C++中的一条语句可能会编译成许多指令。即使是像下面这样简单的语句:

x = y;

上面的语句可能需要在CPU级别执行大量的工作, 这取决于x和y在内存中的位置,它们是否有相同的类型等等。

因此,我们使用高级语言(C/C++)编写的"指令"的数量和CPU实际执行的指令的数量上存在很大的差异。

但是汇编语言所编写的指令和cpu实际执行的指令是一一对应的。汇编语言程序中的每一行代码都保证翻译成单个 CPU 指令。一方面,这意味着汇编语言可以让我们很好的掌握CPU正在执行的工作, 另一方面,我们在C/C++语言中很方便实现的特性实际上在CPU层面都不存在。实际上,在CPU层面上并没有所谓的for循环,if-else条件分支,变量声明等等。我们必须通过组合一些原始的操作来实现这些高级语言的特性。

由于汇编指令和CPU指令是一一对应的,因此每种类型的CPU都有自己对应的汇编语言。Intel CPU的汇编语言和ARM CPU(大多数智能手机使用ARM CPU)的汇编语言是完全不同的。并且与Arduino上使用的AVR CPU完全不同。与C/C++不同的是,汇编语言是无法做到移植性的。

即便我们使用同一种类型的CPU,也不能保证汇编器与操作系统之间的可移植性。与C/C++不同的是, C/C++由国际委员会决定C/C++的标准,然而汇编语言却不是这样。因此,按照YASM(汇编器)写出来的汇编代码可能无法在GAS/NASM或者微软的汇编器上进行汇编。操作系统层面也会导致这样的不兼容性,因为没有"标准汇编库"。在一种操作系统下写出的汇编程序可能无法移植到其他操作系统下。Windows下使用汇编语言写出的程序移植到Linux下可能不能运行,不仅仅是因为汇编器不同,操作系统系统的接口不同也是一个重要原因。(Windows和Linux对于系统调用的定义不同)。

本课程使用的是 YASM 汇编器, 基于64位的Intel CPU(X86-64),在Linux系统下运行。

我们会使用GDB调试器去调试你的汇编程序。在C++中,你最初用于调试程序的工具可能是在出错的位置附近添加cout,但是将打印添加到汇编程序中就可能需要重写你需要打印的函数,甚至重新所有的程序。显示通过打印的方式调试程序在汇编语言的debug中是不可行的。所以我们在课程中也会熟悉GDB工具。

计算机组织架构

计算机组织架构是指计算机的内部结构。内存、CPU、I/O设备等如何连接在一起,如何配合起来工作。虽然我们主要关注我们实际使用的计算机的组织架构(x86-64),但是有时我们也会去和其他的计算机系统进行比较(MIPS, ARM等等)。当然,记住这些不同的系统的区别也很重要。

数字电路

CPU通过数字电路来实现,数字电路由逻辑门电路组成。这是比汇编语言更加底层的内容。我们会稍微了解一下数字电路,仅仅是为了感受CPU是如何进行工作的,但是课程的侧重点还是在汇编语言上。

术语回顾

字节(Byte):可以单独寻址的计算机内存的最小单位。对于我们来说,一个Byte等于8个bit。但是需要了解的是并不是所有的系统都是这样的。有一些奇怪的系统,一个byte是10个bit或者7个bit。

每个byte中的每个bit的位置从右到左编号为0到7:

Bit value	0	0	1	0	1	1	0	1
Bit pos.	7	6	5	4	3	2	1	0

(word): 两个字节(16 bits)。
将一个字视作2个字节时,我们将第一个字节(占据低8位的字节)称之为"低字节", 将第二个字节称之为"高字节"。

word

类似的,如果我们对一个字节中的bit的位置进行编号,则低字节的bit将编号为0-7,而高字节中的bit将编号为8-15。

这个规则可以推广到双字(dword)的低位字和高位字、四字(qword)的低位和高位双字等等。类似的,在一个字节中,比特0代表低位, 比特7代表高位。

双字(double-words/dword): 4个bytes(32个bits)

四字(Quad-word/qword): 8个bytes(64个bits)。(这个quad可以用quadra kill四杀来辅助记忆)

依次类推还有,双四字(double-quad-words),16bytes, 四四字(“quad-quad-words”), 32 bytes, 等等。但是这些很少见,不常用。

KB:kilo-bytes(千字节), 这里的"kilo"指的是二进制的千, 2 10 = 1024 {2}^{10} = 1024 210=1024字节。K后面跟着的大写的B代表我们的单位是字节,如果是小写的b则代表是比特。

Kb:kilo-bit(千比特)。这个单位通常使用的不太多,在通讯领域中使用很多。例如带宽通常以兆比特为单位进行测量。

MB:Mega-byte(兆字节), 2 20 = 1024 2 = 1048576 {2}^{20} = {1024}^{2} = 1048576 220=10242=1048576字节。这个数量级大约是100万字节。

GB: Gigabytes(千兆字节), 2 30 = 1024 3 = 1073741824 {2}^{30} = {1024}^{3} = 1073741824 230=10243=1073741824字节。这个数量级大约是10亿字节。

以此类推还有TB、PB等等。

二进制的百万(million 1048576)和十进制的百万(1000000)的区别就解释了磁盘标签上的容量和系统中实际显示的容量的区别。操作系统使用二进制的度量方式,而标签上印刷的是十进制的度量方式。所以区别磁盘标签上的500GB在你的操作系统中显示的容量将会是下面的数值:

     500,000,000,000
    ——————————————— = 465 GB
     1,073,741,824

数制

十进制(Decimal):十进制以10为基数,这是我们经常使用的。数字范围是0-9。

二进制(Binary): 二进制的数字范围是0-1。

八进制(Octal):八进制的数字范围是0-7。(通常八进制的使用相对较少)

十六进制(Hexadecimal):十六进制的数字范围是0-9,a(10), b(11), c(12), d(13), e(14), f(15)。

接下来我们将回顾二进制和十六进制算术。

注意,这些数制,没有那种一定比其他的类型更好或更正确。

   21   ==   10101b   ==   0x15   ==   025
decimal      binary        hex.       octal

在计算机系统的内部,计算机使用二进制存储内容。但是这个通常对于上层语言(包括汇编语言)而言,并不感知。 我们可以很容易的加减二进制的数字或者其他进制的数字。所以大多数时候,计算机底层使用二进制并不会太影响我们编程。

C/C++ 和汇编都允许我们在上述任何数字系统的源代码中编写数字,只需使用不同的格式:

符号数制
21十进制
10101b二进制,以b结尾
0x15十六进制,以0x开头
025八进制,以0开头

注意,b、0x等不是数字本身的一部分,它们仅仅用来区分不同的进制。编译器/汇编器负责将对应的数字转换为计算机使用的内部格式。

例如,在下面的例子中,你可以这样做:

int x = 21;

if(x == 0x15) {}

上面的if语句中的语句总是为true。

类似地,当我们打印一个数字(通过 cout 或 printf)时,它通常打印为十进制,但通过各种标志我们可以要求十六进制。运行时库负责将内部表示形式转换回十进制/十六进制。在本学期晚些时候,我们将有一个手动打印数字的作业(因为汇编语言没有标准库来为我们做这件事!)

数字电路

CPU 由一组复杂的数字电路实现。数字电路是由逻辑门构建的(逻辑门又是使用晶体管构建的)。在数字电路设计中,在数字电路设计中,我们展示逻辑信号(开/关值)如何从输入流经逻辑门到输出。如果有电流流过逻辑信号,则逻辑信号为高(开);如果没有电流(或电流非常小),则逻辑信号为低(关)。

逻辑门的基本类型有:

  • 非门(NOT):单输入、单输出门,反转其输入。如果输入为高电平,则输出为低电平,反之亦然。
    NOT-gate

    非(NOT) 在 C/C++ 中运算符是~。这个符号是按位非,与逻辑非(!)不同。

  • 与门(AND):双输入、单输出门:当且仅当两个输入均为高电平时,输出为高电平,否则为低电平。

    AND-gate

    AND 的 C/C++ 运算符是 &(这是按位与,与 && 逻辑与 不同)。

  • 或门(OR):双输入、单输出门:如果其中一个或两个输入都为高电平,则输出为高电平,否则(如果两个输入均为低电平)输出为低电平。

    OR-gate

    C/C++ 中与运算符是 | (这又是按位或,不同于逻辑或 ||)

  • 异或门(XOR):双输入,单输出门。如果其中一个输入为高电平但不是两个输入都是高电平,则输出为高电平。否则,当两个输入都为高电平或者两个输入都是低电平,则输出为低电平。实际上,如果输入不同(一高一低),则输出为高,如果输入相同,则输出为低。

    XOR-gate

    C/C++中代表异或的运算符是^(这个是按位异或, 没有逻辑上的异或)。 注意^不是求幂运算符,C/C++中没有求幂的运算符。

  • 与非门(NAND):输出端带有非门的与门。也就是说,如果两个输入都为高电平,则输出为低电平,否则为高电平。

    NAND-gate

    C/C++没有直接的与非运算符。可以使用&~组合起来起到相同的效果。

  • 或非(NOR):在或门的输出端带有一个非门。如果两个输入均为低电平,则输出为高电平,否则为低电平。

    NOR-gate

    C/C++没有直接的或非运算符。可以使用|~组合起来起到相同的效果。

  • 同或(XNOR):输出端带有非门的异或门。如果两个输入相同(均为低电平或均为高电平),则输出为高电平,否则为低电平。

    XNOR-gate

    C/C++没有直接的同或运算符。可以使用^~组合起来起到相同的效果。

你可能会熟悉前三种逻辑门。有几点需要注意:

  • 与门(AND)和或门(OR)可以扩展为超过2个输入端,n输入的与门,当它的所有的n个输入端都是高电平时,则该与门输出高电平,否则为低电平。同样,一个n输入端的或门,只要有一个输入端是高电平,则该或门将输出高电平。如果所有的输入都是低电平,则该或门输出低电平。

下图说明了如何构建 3 输入与门:

3-input-AND-gate

问题:如果异或门以相同的配置排列,所得的 3 输入、1 输出电路会起什么作用?

  • 与非门和或非门具有通用性:所有其他门都可以仅由 NAND 或 NOR 构建。事实上,为了简化制造,仅使用 NAND 门构建电路是很常见的。

    例如,下面是一个相当于仅使用 NAND 门实现的 A OR B 的电路(您应该验证该电路是否为输入 A 和 B 的所有四种组合生成正确的输出)

    NAND-OR-gate.png

    您可以在维基百科上找到有关如何将所有其他类型的逻辑门转换为 NAND 和 NOR 门的完整参考。作业 1 将要求您将使用 NOT、AND 和 OR 的电路转换为仅使用 NAND 门的电路。

电路真值表

任何(无状态)m 输入、n 输出电路的行为也可以使用表格来说明,该表格显示每个输入组合如何映射到特定的输出集。因为每个输入可以是低 (0) 或高 (1),所以该表将有 2m 行和 m + n 列。例如,上面显示的 3 输入 AND:

3-input-AND-gate

InputOutput
ABCQ
0000
1000
0100
1100
0010
1010
0110
1111

从表格中可以知道,仅当所有三个输入均为高电平 (1) 时,输出才为高电平 (1)。

硬件电路

如果您尝试在实际电子硬件中实现逻辑电路,您会遇到上面未提及的几个问题:

为了解决这个不可预测的时期,大多数数字电路都是同步的:他们使用时钟来控制何时执行计算。时钟是一个 0 输入、1 输出的逻辑器件,它输出一个信号,该信号以规则的时钟速率交替出现低、高、低、高……
通常,当时钟信号从低电平变为高电平(时钟信号的“上升沿”)时,电路的其余部分将执行其计算,但直到下一个时钟周期的上升沿才会读取计算的输出。

  • 电流不仅仅从 A 点流到 B 点(如逻辑图所示),而且仅在存在闭合电路时才流动。为了使得电路在实际中能够工作,必须提供电路的最终输出返回到输入电源之间的连接。在真实的电路中,这些连接当然会存在,但在逻辑图中,我们忽略它们,因为它们不会影响电路的逻辑及其实际计算的内容。
    许多门电路都需要电源连接(始终为高电平的输入)为其供电,这会使得现实中的电路更加复杂。
  • 如果你想尝试只购买一个或门, 你会发现你无法仅仅买一个或门。门电路通常是在集成电路上使用,通常会将多个相同类型的门电路绑定在一起。例如,您可以购买在单个芯片上具有四个、八个或更多 NAND 门的 IC(集成电路)。这是有道理的,因为在实际的电路设计中,您很少只需要一个门。(该芯片将具有一个由所有栅极共享的单电源输入)。
  • 理想情况下,我们将逻辑电路描述为信号瞬间从低电平切换到高电平,反之亦然,但在现实系统中这是不可能的。电路的上升时间是线路从低电平变为高电平所需的时间。在此过渡期间,流过连接的电流量介于 0 和 1 之间,这可能会导致电路输出在短时间内不可预测。
  • 为了解决这个不可预测的时期,大多数数字电路都是同步的:他们使用时钟来控制何时执行计算。时钟是一个 0 输入、1 输出的逻辑器件,它输出一个信号,该信号以规则的时钟速率交替出现低、高、低、高……通常,当时钟信号从低电平变为高电平(时钟信号的“上升沿”)时,电路的其余部分将执行其计算,但直到下一个时钟周期的上升沿才会读取计算的输出。因此,输出有 1 个完整时钟周期来稳定在正确的值。
    事实上,即使信号很高,它仍然不会处于恒定水平;它只是高于某个标记“低”和“高”之间分界线的阈值。
  • 在电气方面,单个输出无法连接到无限数量的其他设备;输出的“扇出”是有限制的。
  • 逻辑门可以通过多种不同的方式以电子方式实现,从而产生不同的逻辑系列,每个都有自己的电气特性。例如,对于不同的系列,“低”与“高”的电压水平可能非常不同。另请注意,在大多数系列中,“低”电平不是 0V,而是低于“高”电平的某些电压电平。例如,晶体管-晶体管-逻辑 (TTL) 系列使用 0 至 0.8V(相对于地)之间的低电压电平,以及 2 至 5V 的高电压电平。 0.8 至 2V 之间的输入信号处于“不可预测”范围内,可能会被视为高或低,甚至在两者之间波动。

数字电路的问题

您可以尝试构建以下一些电路,以测试您对逻辑电路的理解:

  • 使用你喜欢的任何逻辑门器件,去构建一个4输入 1输出的电路, 当且仅当其中一个输入为高电平时才输出高电平。
  • 使用你喜欢的任何逻辑门器件,去构建一个4输入 1输出的电路, 当且仅当有两个输入为高电平时才输出高电平。
  • 仅使用 NAND,构建一个比较器电路,一个 2 输入、1 输出电路,如果满足以下条件,则输出为高电平:
    • 两个输入都是低电平
    • 第一个输入为高电平,第二个输入是低电平
    • 两个输入都是高电平
      这等价于检查是否第一个输入小于等于第二个输入。

这些问题有许多不同的可能解决方案。数字电路的进阶课程将教授优化电路设计的方法,以便最大限度地减少所使用的门的数量。

汇编语言的开始

这里我们将使用汇编语言去编写一个经典的程序: Hello World程序。我们可以使用两种广泛的风格来编写汇编(.asm 程序)。

  • 我们可以通过调用操作系统的系统调用来与操作系统交互。由于缺少更好的名称,我们称之为系统调用风格(syscall-style)。这是最直接的方法,但是操作起来不太方便。如果我们使用这种方式,那么我们的汇编程序的入口程序就是_start,我们首先使用系统调用向标准输出打印一个字符串,使用另外一个系统调用退出。

    如果我们使用系统调用风格,我们的程序将是完全独立的:除了我们编写的内容之外,生成的可执行文件中不会有任何内容。

  • 我们可以使用标准c库中的方法例如printfexit。这称之为"C库风格"。这就需要我们自己去链接c语言库。这个方法显然要强大得多,因为它将c标准库中的所有资源都给了我们的程序。

    如果我们使用C库的风格,那么最终生成的可执行文件将不仅包括我们编写的代码,还包括标准库添加的很多的代码。

下面我们先使用第一种风格(系统调用风格)编写程序,这个方式上手更快一些。

;;; 
;;; hello.s
;;; Prints "Hello, world!"
;;;

section .data

msg:            db      "Hello, world!", 10
MSGLEN:         equ     $-msg

section .text

;; Program code goes here

global _start
_start:

    mov     rax,    1               ; Syscall code in rax
    mov     rdi,    1               ; 1st arg, file desc. to write to
    mov     rsi,    msg             ; 2nd arg, addr. of message
    mov     rdx,    MSGLEN          ; 3rd arg, num. of chars to print
    syscall

    ;; Terminate process
    mov     rax,    60              ; Syscall code in rax
    mov     rdi,    0               ; First parameter in rdi
    syscall                         ; End process

可以使用下面的命令进行汇编和链接:

asm hello.s

也可以手动进行:

yasm -g dwarf2 -f elf64 hello.s -l hello.lst
ld -g -o hello hello.o

然后执行像下面这样执行:

./hello

将打印出下面这样的内容:

Hello, world!

打印后会退出。

一步一步分解该程序,每行均包含以下形式:

label:       instruction       ; comment

所有这些内容都是可选的,因此只有几行是以label开头, 并且很多行没有注释。 Label后面的冒号(:)也是可选的,但是为了程序的清晰,最好写上。

解释
section .datadata节,包含初始化的常量和变量
msg: db “Hello, world!”, 10msg定义了一个指向"Hello,world!“字符串的标签,它将被逐字复制到我们的汇编程序中。db代表"define byte”,即定义字节,最后的10,在ascii表中代表LF(\n)。注意用汇编的 db 伪指令定义字符串,不会自动添加"\0",这个要和C/C++相区别
MSGLEN: equ $-msgequ定义了一个常量叫做MSGLEN, 这个常量代表的是msg的长度, $代表当前的位置
section .texttext节定义了程序的实际执行的代码
global _start我们将_start标签声明为全局,以便在程序之外可见(以便操作系统可以找到它可以启动我们的程序)
_start这将 _start声明为指向程序中当前位置的标签
mov rax, 1这会将值 1 加载到寄存器 rax中,该寄存器存储系统调用代码。 1 是"写入文件"的系统调用代码
mov rdi, 1将 1 存储到寄存器 rdi中。这是系统调用 write的第一个参数,它是文件描述符(1 是标准输出)
mov rsi, msgmsg、地址存储到 rsi中, 这是第二个参数,表示要写的消息
mov rdx, MSGLENMSGLEN存储到 rdx中。这是第三个参数,即要写入的长度(以字节为单位)
syscall调用rax中存储的值所代表的系统调用,打印字符
mov rax, 6060 是"退出进程"的系统调用代码
mov rdi, 0第一个参数,0,退出代码(成功)
syscall执行系统调用

注意: 对于汇编语言而言,默认的后缀是.s

对于Intel 语法而言, mov指令的结构如下所示:

mov dest, src

即mov后先跟着目的对象,再接着是源对象。将其理解为dest = src是可以的。

程序的节(sections)

内存中正在运行的程序,其内存空间分为许多不同的"部分"。尽管所有部分都是同一地址空间的一部分,但它们在概念上用于不同的用途,并且操作系统可能对其应用不同的权限。例如,操作系统通常将.text部分(可执行机器代码所在的位置)设置为只读,因为自修改代码(通常)要么是错误,要么是漏洞利用。

一个进程的内存布局通常如下所示:

--------------------
Stack (grows down)
…
Heap (grows up)
---------------------
.data section (global variables)
---------------------
.text section
---------------------

栈向下增长这一点很重要, 这代表压栈操作会使得栈顶指针减少。

除了用于存放全局变量的.data节之外,还有一块是.bss节,其用于存放未经过初始化的全局数据。.data.bss的区别在于当程序运行时,操作系统会将.data中的数据从磁盘中拷贝到内存中。而.bss节中由于存放的是未经过初始化的数据,因此操作系统不需要复制任何内容,只需要预留好对应的空间即可。(还有一些其他类型的节,不过我们目前不会用到,例如.readonly)

通过定义更多常量,可以使上面的程序更容易阅读,例如:

section .data

SYS_write       equ     1
SYS_stdout      equ     1
SYS_exit        equ     60
EXIT_SUCCESS    equ     0

equ定义了一个汇编时的常量,当程序运行的时候,这些常量不会在内存中占据任何的空间。 这有点类似与C/C++语言中的#define

使用它们时,我们只需通过名称来引用它们:

mov     rax,  SYS_exit 
mov     rdi,  EXIT_SUCCESS
syscall

db将字节序列直接存储到可执行文件中, 例如下面的代码:

msg  db  "Hello, world!", 10

其实际上做了两件事情:

  • "Hello,world!"连同后面的10一同被写入了可执行文件中。
  • msg定义了一个标签,该标签指向了字节序的开始的地址。注意我们并不是将字符串的存入了msg中,而是将字符串的地址存入了msg中。

因为我们使用系统调用风格,所以我们的字符串不以终止符 NULL (\0) 字符结尾。(上面的字符串以 10 结尾,即换行的 ASCII 字符;这就是在 C/C++ 中使用 \n 字符转义时得到的结果。) 我们必须知道要传递给 SYS_write 系统调用的字符串的长度。我们可以简单地手动计算字节数,但如果我们更改字符串的内容,就需要重新计算字符串的长度。

如前所述,汇编器将字符串 msg放入生成的可执行文件中的某个地址。事实上,我们的汇编源文件中的所有内容都有一些地址,它将最终出现在生成的可执行文件中。即使像 MSGLEN这样理论上占用 0 空间的东西也有输出文件中"当前位置"的一些概念。$获取当前位置的地址。$-msg从当前地址减去地址msg,得到 msg 指向的字符串的长度。需要注意的是,这只在定义 msg之后立即定义了 MSGLEN才会有效;如果中间有任何其他定义占用了文件中的空间,则计算出的长度将是错误的。

(这也表明 equ定义可以在其值中使用有限的算术;计算是在汇编时完成的,而不是在运行时完成的。)

在我们所有的程序中,我们首先是 .data部分,然后是 .text部分,但这只是一个约定。您可以更改各部分的顺序,甚至可以将它们交错排列,您的程序仍然可以运行。

调用操作系统的系统调用

调用系统调用的过程如下:

  • rax设置为要执行的系统调用的编号。例如SYS_exit的系统调用编号为60, 而SYS_write的系统调用编号为1。你可以在这里找到所有系统调用的编号。
  • rdirsirdxr10,r8,r9设置为系统调用函数的第一个、第二个、第三个参数。往后以此类推。
  • 执行系统调用```syscall``指令。

请注意,步骤 (1) 和 (2) 可以按任何顺序发生,但在执行系统调用之前必须正确设置所有寄存器值。如果系统调用返回一个值(SYS_writeSYS_exit都没有),则系统调用返回后该值将位于 rax中。

列出文件(Listing files)

yasm命令中的 -l noop.lst参数是可选的;它指示 YASM 生成一个列表文件,这是我们逐行编写的汇编指令及其十六进制操作码的列表。以下是上述程序的列表文件:

     1                                 %line 1+1 hello_bare.s
     2                                 
     3                                 
     4                                 
     5                                 
     6                                 
     7                                 [section .data]
     8                                 
     9 00000000 48656C6C6F2C20776F-    msg db "Hello, world!", 10
    10 00000000 726C64210A         
    11                                 MSGLEN equ $-msg
    12                                 
    13                                 [section .text]
    14                                 
    15                                 
    16                                 
    17                                 [global _start]
    18                                 _start:
    19                                 
    20 00000000 48C7C001000000          mov rax, 1
    21 00000007 48C7C701000000          mov rdi, 1
    22 0000000E 48C7C6[00000000]        mov rsi, msg
    23 00000015 48BA0E000000000000-     mov rdx, MSGLEN
    24 00000015 00                 
    25 0000001F 0F05                    syscall
    26                                 
    27                                 
    28 00000021 48C7C03C000000          mov rax, 60
    29 00000028 48C7C700000000          mov rdi, 0
    30 0000002F 0F05                    syscall

第一列是原始行号,第二列是汇编程序中相对于当前节的地址(从 00000000 开始),第三列是操作码,第四列是我们的原始程序。

从这里看, mov rax, 60的操作码是48C7C03C000000mov rdi, 0操作码是48C7C700000000 , syscall的操作码是0F05。(x86-64 使用不同的指令宽度:并非所有操作码的字节数都相同;有些较短,有些较长)

汇编和链接

asm脚本负责在所有输入文件上运行汇编程序,然后将它们链接在一起。 它还能正确检测您是否将 _startmain定义为程序的入口点,并在后一种情况下与C 标准库链接。

如果你想进行手动汇编,则需要执行的命令如下所示:

yasm -g dwarf2 -f elf64 filename.s -l filename.lst
  • -g 参数给出了调试信息使用的格式,以便 GDB(参见下一节)可以读取它。
  • -f 参数表示输出 x86-64 格式的目标文件。
  • -l 参数表示输出列表文件。

要将一个(或多个)汇编的目标文件链接到一起成为可执行文件,有两种选择:

  • 如果您没有使用任何 C 标准库函数,并且程序的入口点名为 _start,则使用 ld:

    ld -g -o exe_name object.o files.o ...
    
  • 如果您使用的是 C 标准库中的函数,并且入口点名为 main,则使用 gcc:

     gcc -o exe_name object.o files.o ...
    

    这与用于链接 C 程序的目标文件的命令行相同。 (事实上​​,它可以用来链接由 C 和汇编语言混合组成的程序!)

(asm 脚本检查是否有任何文件定义了 main;如果定义了 main,则假定您要使用 C 标准库函数。)

调试汇编程序

GDB可以识别汇编语言,我们可以通过以下方式在 GDB 中运行我们的程序

gdb ./hello

我们可以通过以下方式在程序的_start处打上断点:

break _start
run

然后使用 n(next) 命令逐行执行程序。寄存器的值可以按名称打印,前缀为 $,例如

print $rax

或更改它们:

set $rax = 0

您还可以使用info registers一次打印所有寄存器。

请注意,当我使用 GDB 时,我使用一个名为GDB dashboard的插件,它显示每一步的寄存器内容。

GDB 将默认使用 AT&T语法进行汇编。您可以通过输入命令将其切换为 Intel语法

set disassembly-flavor intel

您可以将此命令放入 ~/.gdbinit中,以便所有GDB会话都会有这样的设置。

反汇编现有的程序

您可以使用 objdump反汇编已编译的可执行文件。这对你弄清楚C/C++程序在汇编层面如何执行非常有用。当然,结果通常并不像您想象的那么有用, 因为编译器可能对您的代码做了一些"有趣"的事情。编译器的目标是使生成的程序集更快,但并不容易理解。

#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    return 0;
}

使用如下的命令进行编译:

gcc -c hello.c

这里会产生hello.o的目标文件。我们可以对其反汇编:

objdump -d -M intel hello.o

输出如下:


hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   bf 00 00 00 00          mov    edi,0x0
   9:   e8 00 00 00 00          call   e <main+0xe>
   e:   b8 00 00 00 00          mov    eax,0x0
  13:   5d                      pop    rbp
  14:   c3                      ret  

这里看不到太多有用的东西。因为它依赖了很多标准库提供的很多方法的实现, 而目前我们还没有对它们进行链接。目前对于调用printf的地方,只是用了占位符进行替代。

上面的步骤,我们只是对目标文件进行了反汇编,而不是链接后的可执行文件。

我们可以对hello.o链接成一个可执行文件,执行如下的命令:

gcc -o hello hello.o

然后使用objdump -d -M intel hello,我们将会得到更多的汇编内容。标准库在运行 main()之前做了很多设置,并且可执行文件包含所有这些代码。另一方面,它让我们看到了最终的 main是什么样子的:

0000000000400507 <main>:
  400507:   55                      push   rbp
  400508:   48 89 e5                mov    rbp,rsp
  40050b:   bf a4 05 40 00          mov    edi,0x4005a4
  400510:   e8 eb fe ff ff          call   400400 <puts@plt>
  400515:   b8 00 00 00 00          mov    eax,0x0
  40051a:   5d                      pop    rbp
  40051b:   c3                      ret    
  40051c:   0f 1f 40 00             nop    DWORD PTR [rax+0x0]

从连接后的反汇编的代码中,我们知道:

在这里,空的调用已经被替换为过程调用的puts(puts@plt), 这个过程调用的内部实现了printf的功能。

main是一个过程;它是从_start标签(标准库提供) 调用的,并且必须在完成后返回,因此它以ret指令结束。

前两条指令push rbp; mov rbp,rsp也是每个过程开始的标准写法,这个在后续的章节中会学习到。

阅读反汇编的其余部分,你可以研究 _start过程以及 puts的定义。

附录

课程资源

课程原文: https://staffwww.fullcoll.edu/aclifton/cs241/lecture-introduction.html

相关课程: https://www.cs.uaf.edu/2017/fall/cs301/lecture/09_13_memory.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/527186.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot项目 jar包方式打包部署

SpringBoot项目 jar包方式打包部署 传统的Web应用进行打包部署&#xff0c;通常会打成war包形式&#xff0c;然后将War包部署到Tomcat等服务器中。 在Spring Boot项目在开发完成后&#xff0c;确实既支持打包成JAR文件也支持打包成WAR文件。然而&#xff0c;官方通常推荐将Sp…

LeetCode初级算法书Java题解日常更新

LeetCode初级算法高效题解&#xff08;含思路注释&#xff09; 文章目录 LeetCode初级算法高效题解&#xff08;含思路注释&#xff09;前言一、数组1.删除排序数组中的重复项2.买卖股票的最佳时机 II3.旋转数组4.存在重复元素 总结 前言 决定用四个月过一下算法 一、数组 1.…

下载python电子书

下面展示一些 内联代码片。 import requests from lxml import etree from urllib import parse from pprint import pprint from tqdm import tqdm class PythonBook: def init(self): self.url“https://m.jb51.net/books/list476_1.html” self.url_page“https://m.jb51.n…

二维码门楼牌管理应用平台:促进二手交易市场的透明化与规范化

文章目录 前言一、二维码门楼牌管理应用平台的建设背景二、二维码门楼牌管理应用平台的功能特点三、二维码门楼牌管理应用平台在二手交易市场中的应用四、二维码门楼牌管理应用平台的未来展望 前言 随着互联网的快速发展&#xff0c;二维码技术已广泛应用于各个领域。在二手交…

【操作系统】python实现银行家算法

银行家算法是最具有代表性的避免死锁的算法。 1、算法原理 银行家算法&#xff1a;当一个新进程进入系统时&#xff0c;该进程必须申明在运行过程中所需要的每种资源的最大数目&#xff0c;且该数目不能超过系统拥有的资源总量。当进程请求某组资源时&#xff0c;系统必须先确…

HarmonyOS 应用开发-自定义Swiper卡片预览效果实现

介绍 本方案做的是采用Swiper组件实现容器视图居中完全展示&#xff0c;两边等长露出&#xff0c;并且跟手滑动效果。 效果图预览 实现思路 本解决方案通过维护所有卡片偏移的数组&#xff0c;实时更新卡片的偏移量&#xff0c;以实现swiper子组件内图片居中展示&#xff0c…

DHT11温度检测系统

DHT11温湿度传感器 产品概述 DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器&#xff0c;应用领域&#xff1a;暖通空调&#xff1b;汽车&#xff1b;消费品&#xff1b;气象站&#xff1b;湿度调节器&#xff1b;除湿器&#xff1b;家电&#xff1b;医…

好物推荐:六款让人眼前一亮的个人博客

1.前言 总是有人在问零基础如何搭建个人博客、有哪些好用的博客系统推荐、个人博客和国内技术社区怎么选择&#xff1f;诸如此类的很多问题。对于最后一个问题&#xff0c;我个人的看法很简单&#xff0c;看需求&#xff01; 目前国内做的还不错的技术类社区/论坛其实还是比较…

stack和queue的使用

前言 前面我们对string、vector、list做了介绍并对底层进行了实现&#xff01;本期我们继续来介绍STL容器&#xff0c;stack和queue&#xff01; 本期内容介绍 stack 常用接口的介绍 queue 常用接口的介绍 什么是stack? 这里的栈和我们C语言实现的数据结构的那个栈功能是一样…

RabbitMQ-死信队列常见用法

目录 一、什么是死信 二、什么是死信队列 ​编辑 三、第一种情景&#xff1a;消息被拒绝时 四、第二种场景&#xff1a;. 消费者发生异常&#xff0c;超过重试次数 。 其实spring框架调用的就是 basicNack 五、第三种场景&#xff1a; 消息的Expiration 过期时长或队列TTL…

neo4j使用详解(十一、cypher自定义函数语法——最全参考)

Neo4j系列导航&#xff1a; neo4j安装及简单实践 cypher语法基础 cypher插入语法 cypher插入语法 cypher查询语法 cypher通用语法 cypher函数语法 neo4j索引及调优 10.自定义函数 用户定义函数用Java编写&#xff0c;部署到数据库中&#xff0c;并以与任何其他Cypher函数相同的…

Java变量详解

​ 这里写目录标题 第一章、Java中的变量分类1.1&#xff09;变量分类1.2&#xff09;成员变量分类1.3&#xff09;成员变量和局部变量的区别 第二章、成员变量详解2.1&#xff09;成员变量作用域/权限修饰符2.2&#xff09;成员变量和成员属性的区别2.3&#xff09;成员变量初…

网络通信流程

建立完tcp请求再发起http请求 开启系统代理之后&#xff0c;以clash verge为例 127.0.0.1:7897&#xff0c;假设hci.baidu.com的IP为153.37.235.50 发起对hci.baidu.com的HTTP请求&#xff0c;由于开启了系统代理不进行DNS解析&#xff0c;浏览器调用socket()获得一个socket&a…

GlusterFS(GFS)分布式文件系统

一、GlusterFS的概述&#xff1a; GlusterFS 是一个开源的分布式文件系统。 只在扩展存储容器&#xff0c;提高性能 并且通过多个互联网络的存储节点的进行几余&#xff0c;以确保数据的可用性和一致性 由存储服务器、客户端以及NFS/Samba 存储网关&#xff08;可选&#xff0c…

软考中级之软件设计师---知识点汇总总结

软考中级之软件设计师---知识点汇总总结 软考介绍资格设置证书样本 计算机组成原理操作系统1. 进程的三态模型2. 磁盘调度算法 计算机网络1. 网络的分类2. 各层的互连设备3. 网络模型&#xff0c;协议簇4. 传输层协议TCP、UDP4.1 TCP (Transmission Control Protocol,传输控制协…

零代码与低代码开发平台

1、什么是低代码开发平台&#xff1f;什么是零代码开发平台&#xff1f; 零代码开发平台&#xff1a; 指的是不需要写代码就能够快速开发出业务应用/系统的平台。我们在工作中使用的业务应用&#xff0c;主要提供数据收集、数据处理、数据流转和展示等功能。零代码开发平台能够…

【超重磅牛市信号】减半倒计时12天!首波抛售潮接近尾声,大暴涨将如期而至!

3月&#xff0c;美国CPI环比出现小幅反弹由3.1%升至3.2%&#xff0c;美国制造业指数PMI反弹至50.3%呈现进入扩张期的态势&#xff0c;日本结束长达8年的负利率时代首次加息。这导致美国4月降息概率大幅下降&#xff0c;5月降息概率也跌至50%以下。 尽管如此&#xff0c;全球金融…

C#操作MySQL从入门到精通(8)——对查询数据进行高级过滤

前言 我们在查询数据库中数据的时候,有时候需要剔除一些我们不想要的数据,这时候就需要对数据进行过滤,比如学生信息中,我只需要年龄等于18的,同时又要家乡地址是安徽的,类似这种操作专栏第7篇的C#操作MySQL从入门到精通(7)——对查询数据进行简单过滤简单过滤方法就无法…

STL优先队列比较器

有两个比较器&#xff0c;在std里面&#xff0c;一个是greater&#xff0c;一个是less&#xff0c;他们都有一个可以指定的模板类型。 #include <bits/stdc.h> using namespace std; struct node {bool operator ()(const string& a, const string& b){return a…

蓝桥杯刷题-特殊年份

特殊年份 代码&#xff1a; def f(x)->bool:s list(x)if s[0]s[2] and int(s[1])1int(s[3]):return Trueelse:return False cnt 0 for _ in range(5):if f(input()):cnt1 print(cnt)