汇编语言学习

这玩意儿就是写个备忘录,结构拉稀是正常的

入门

啥是寄存器?

CPU 中存储计算数据相关信息的地方,CPU 有很多寄存器,帮助记忆计算的数据是什么,是做什么运算。

用 vim 打开一个新文件并写入

Ubuntu 环境下,首先进入终端界面

然后使用 vim 【文件名.后缀名】 在 vim 中打开新文件

现在是无法输入的,必须按下 i 键进入 insert 模式

输入完成后,按下 esc 键退出输入模式

使用 :wq 保存文件

指令

发送给 CPU 的一个命令,前提是 CPU 有对应功能

  • mov

数据传送指令,我们可以像下面这样用 mov 指令,达到数据传送的目的。

1
2
3
mov eax, 1          ; 让eax的值为1(eax = 1)
mov ebx, 2 ; 让ebx的值为2(ebx = 2)
mov ecx, eax ; 把eax的值传送给ecx(ecx = eax)

可以将数据送入寄存器,也可以将一个寄存器的数据送到另一个寄存器:

1
2
mov eax, 1
mov ebx, eax
  • add

加法指令

1
2
add eax, 2          ; eax = eax + 2
add ebx, eax ; ebx = ebx + eax
  • sub

减法指令(用法和加法指令类似)

1
2
sub eax, 1              ; eax = eax - 1
sub eax, ecx ; eax = eax - ecx
  • ret

返回指令,类似于 C 语言中的 return,用于 函数调用后的返回

更多寄存器

除了前面列举的eax、ebx、ecx、edx之外,还有一些寄存器:

1
2
3
esi
edi
ebp

其中eax、ebx、ecx、edx这四个寄存器是通用寄存器,可以随便存放数据,也能参与到大多数的运算。而余下的三个多见于一些访问内存的场景下,不过,目前,你还是可以随便抓住一个就拿来用的。

内存

暂时存放 CPU 计算所需指令和数据的地方。优点是便宜且容量大,但是处理速度比寄存器慢

寄存器中数据转移至内存

在寄存器中空间不够时,我们会将其数据转移至内存中

同样,我们还是使用 mov 指令:

1
mov [0x5566], eax

最前面那个指令 mov [0x5566], eax 的作用:

将 eax 寄存器的值,保存到编号为 0x5566 对应的内存里去,按照前面的说法,一个 eax 需要 4 个字节的空间才装得下,所以编号为 0x5566 0x5567 0x5568 0x5569 这四个字节都会被 eax 的某一部分覆盖掉

取出时就反着写:

1
mov eax, [0x5566] 

地址存储练习

按照上面的示例,很容易就写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
global main

main:
mov ebx, 1
mov ecx, 2
add ebx, ecx

mov [0x233], ebx
mov eax, [0x233]

ret

然后程序就挂了……

这是因为程序运行环境处于一个受管控的环境下,不能随便读写内存。需要特殊处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
global main

main:
mov ebx, 1
mov ecx, 2
add ebx, ecx

mov [sui_bian_xie], ebx
mov eax, [sui_bian_xie]

ret

section .data
sui_bian_xie dw 0

与前面那个崩溃的程序相比,后者有一些微小的变化,还多了两行代码

1
2
section .data
sui_bian_xie dw 0

第一行先不管是表示接下来的内容经过编译后,会放到可执行文件的数据区域,同时也会随着程序启动的时候,分配对应的内存

第二行就是描述真实的数据的关键所在里,这一行的意思是开辟一块 16bits 字节的空间,并且里面用 0 填充。这里的 dw(define word)定义了16bits的空间,前面那个 sui_bian_xie 的意思就是这里可以随便写,也就是起个名字而已,方便自己写代码的时候区分,这个 sui_bian_xie 会在编译时被编译器处理成一个具体的地址,我们无需理会地址具体时多少,反正知道前后的sui_bian_xie指代的是同一个东西就行了。

反汇编

使用 gdb 需要指令:gdb ./[编译好的文件名]

启动之后,你会看到终端编程变成这样了:

1
(gdb) 

OK,说明你成功了,接下来输入,并回车:

1
(gdb) set disassembly-flavor intel

这一步是把反汇编的格式 调整称为 intel 的格式,稍后完事儿后你可以尝试不用这个设置,看看是什么效果。好了,继续,反汇编,输入命令并回车:

1
2
3
4
5
6
7
8
9
10
(gdb) disas main
Dump of assembler code for function main:
0x080483f0 <+0>: mov eax,0x1
0x080483f5 <+5>: mov ebx,0x2
0x080483fa <+10>: add eax,ebx
0x080483fc <+12>: ret
0x080483fd <+13>: xchg ax,ax
0x080483ff <+15>: nop
End of assembler dump.
(gdb)

好了,整个程序就在这里被反汇编出来了,请你先仔细看一看,是不是和我们写的源代码差不多?(后面多了两行汇编,你把它们当成路人甲看待就行了,不用理它)。

断点操作(下面一大段都是摘得)
  • 断点:程序在运行过程中,当它执行到“断点”对应的这条语句的时候,就会被强行叫停,等着我们把它看个精光,然后再把它放走
  • 注意看反汇编代码,每一行代码的前面都有一串奇怪的数字,这串奇怪的数字指它右边的那条指令在程序运行时的内存中的位置(地址)。注意,指令也是在内存里面的,也有相应的地址。

好了,我们开始尝试一下调试功能,首先是设置一个断点,让程序执行到某一个地方就停下来,给我们足够的时间观察。在gdb的命令行中输入:

1
(gdb) break *0x080483f5

后面那串奇怪的数字在不同的环境下可能不一样,你可以结合这里的代码,对照着自己的实际情况修改。(使用反汇编中<+5>所在的那一行前面的数字)

然后我们执行程序:

1
2
3
4
5
(gdb) run
Starting program: /home/vagrant/code/asm/03/test

Breakpoint 1, 0x080483f5 in main ()
(gdb)

看到了吧,这下程序就被停在了我们设置的断点那个地方,对比着反汇编和你的汇编代码,找一找现在程序是停在哪个位置的吧。run后面提示的内容里,那一串奇怪的数字又出现了,其实这就是我们前面设置断点的那个地址。

好了,到这里,我们就把程序看个精光吧,先看一下eax寄存器的值:

1
2
(gdb) info register eax
eax 0x1 1

刚好就是1啊,在我们设置断点的那个地方,它的前面一个指令是mov eax, 1,这时候eax的内容就真的变成1了,同样,你还可以看一下ebx:

1
2
info register ebx
ebx 0xf7fce000 -134422528

ebx的值并不是2,这是因为mov ebx, 2这个语句还没有执行,所以暂时你看不到。那我们现在让它执行一下吧:

1
2
(gdb) stepi
0x080483fa in main ()

好了,输入stepi之后,到这里,程序在我们的控制之下,向后运行了一条指令,也就是刚刚执行了mov ebx, 2,这时候看下ebx:

1
2
(gdb) info register ebx
ebx 0x2 2

看到了吧,ebx已经变成2了。继续,输入stepi,然后看执行了add指令后的各个寄存器的值:

1
2
3
4
(gdb) stepi
0x080483fc in main ()
(gdb) info register eax
eax 0x3 3

执行完add指令之后,eax跟我们想的一样,变成了3。如果我不知道程序现在停在哪里了,怎么办?很简单,输入disas之后,又能看到反汇编了,同时gdb还会标记出当前断点所在的位置:

1
2
3
4
5
6
7
8
9
(gdb) disas
Dump of assembler code for function main:
0x080483f0 <+0>: mov eax,0x1
0x080483f5 <+5>: mov ebx,0x2
0x080483fa <+10>: add eax,ebx
=> 0x080483fc <+12>: ret
0x080483fd <+13>: xchg ax,ax
0x080483ff <+15>: nop
End of assembler dump.

现在刚好就在add执行过后的ret那个地方。这时候,如果你不想玩了,可以输入continue,让程序自由地飞翔起来,直到GG。

1
2
3
(gdb) continue
Continuing.
[Inferior 1 (process 1283) exited with code 03]

看到了吧,程序已经GG了,而且返回了一个数字03。这刚好就是那个eax寄存器的值嘛。