本文讲述如何用半天时间学会一门汇编的诀窍。在学习汇编过程,最好用Visual Studio调试,打开汇编模式,把栈视图和寄存器视图都打开。函数调用使用
cdecl
,在调试过程中使用汇编单步。
很多程序员都觉得汇编是可怕的编程语言,感觉很难学,繁多的指令,各种寄存器,寻址方式和CPU机制紧密相关,一切都让人望而却步。其实,汇编相对众多编程语言来说,是一门非常简单的语言:它没有奇技淫巧式的语法,也没有各种全家桶式的框架。它之所以显得非常难掌握的原因:
- 它解决的问题,离程序员平时面临的问题太远。
- 目前很多编程语言书籍和资料都是集中该语言本身,很少会和其它语言横向对比和建立联系。讲C语言就是讲C语言,讲C 也只是讲C ,讲汇编也是只是讲汇编。至于C/C 和汇编之间的对比和联系呢?没有。
但在现实生活中,还是有不少地方用到汇编语言,除了搞嵌入式之外,在C/C ,OC之类的语言,在定位程序崩溃,内存泄露,逆向破解,漏洞挖掘和分析,恶意软件分析,都会用到。
所以,还是需要学一下汇编的。如何学呢?重要是把它和程序员平时面临的问题和熟识的语言建立一种联系。这和学数理化差不多,数理化学得好的人,基本上都会把抽象思维和现实世界建立某种联系。
在所有的编程语言中,这三样东西基本上是不可或缺的:
- 函数
- 程序执行顺序
- 数据结构
所以,重要是建立这三样东西在高级编程语言C/C 和汇编的对应关系。对于什么指令,寄存器,寻址方式和CPU机制,就先不管。
函数
在高级编程语言里,函数的参数传递是通过变量或数值,返回值是通过变量或数值。那么在汇编里呢?在汇编里,参数传递和返回结果叫做调用约定。
调用约定传递参数和返回值,是通过寄存器和栈来完成的。那么,就要看传递参数用到什么寄存器,返回值用到什么样的寄存器。由于寄存器数量有限,就演变成这些问题:
- 第一二三四五...这些参数分别用哪些寄存器来传递?有没有个数限制,超过了限制,参数又如何传递?
- 返回值通过哪个寄存器传递?
- 如果通过栈来传递,标志栈的是哪个寄存器?
- 在C 的情况下,成员函数的参数传递又是如何?
- 当前函数桢用哪个寄存器表示?
- 函数执行完,如何返回调用者?
在x86
下,在cdecl
调用方式,基本上,参数都是通过栈来传递,返回值是通过eax
传递,栈是由esp来控制,而this指针是通过ecx
(windows下)或栈(Linux)。函数桢用ebp
指向。返回地址在栈上。
在mips
下,参数都是通过a0-a7
传递的,多余的则放在栈上,通过sp
来指向,而返回值往往一般只通过v0
返回。this指针一般作为第一个参数用a0
传递。函数桢用fp
指向。返回地址放在ra
。
在sparc
下,则会比较奇怪。传递时是通过o0-o6
来传递,但在函数执行时则从i0-i6
来取,当然超过是在放在栈上。而返回值则通过i0
传递,调用者则从o0
来取。栈是通过sp
指向。函数桢用fp
指向,返回地址在i7
。
在iOS
下,参数是通过x0-x3
传递,返回值也是通过x0
。由于没有进行调试,只是在IDA进行逆向,所以其它不清楚。
这上面是我曾经搞过的CPU平台,其中x86
和sparc
是08-10年时,mips
是11年-12年接触的。iOS
是在2020年搞了一天,只是为了看看jailbreak
反检测机制。
从上面来看,可以需要了解的寄存器少了很多,而且需要了解的寄存器都是有关联的。而且这些知识点可以这样掌握:
- 编写没有参数和返回值的函数,只是进行简单的1 1的操作。了解一下编译器会生成哪些汇编
- 编写没有参数有返回值的函数,
return 1 1
的操作,了解返回值是放在哪个寄存器的。 - 编写有参数有返回值的函数,了解一下参数是如何传递,并且把参数的个数不断增加,看看传递改变。
- 编写一个类和一个成员函数,看看
this
指针如何传递。
本人的coredump
系列第三章就是这个思路。详情请见开发目录
程序执行顺序
无论高级语言是怎样的,它编译成二进制文件后,它的执行逻辑应该是不变的。也就是说,顺序执行,在机器码层面还是顺序执行; 条件执行在机器码层面还是条件执行;循环执行在机器码层面还是循环执行;函数调用在机器码层面还是函数调用(不优化,不内联的情况下)。程序的执行顺序就构成了程序的骨架,也就是说,由于汇编和机器码是一一对应的,在汇编中,只要找到if/else/switch/continue/break/while/do/for
之类以及函数调用的对应指令或特征,就可以把汇编和高级语言对应起来。
在x86
下,break, while(1), for( ;; ), continue
对应于jmp
,区别在于break
跳转的地址是向后,if/else/switch/for/do/while
对应于一些jnz, jz, jne, je, jnb, jbe,jg
之类的指令。函数调用是call
,函数返回是ret
。
在mips
下,break, while(1), for( ;; ), continue
对应于j,jr
,区别在于break
跳转的地址是向后, ``if/else/switch/for/do/while对应于
bne,beq,函数调用对应
jal,返回对应于
jr $ra`。
在sparc
下,break, while(1), for( ;; ), continue
对应于ba
,区别在于break
跳转的地址是向后, if/else/switch/for/do/while
对应于bne,be,ble,bg,bge
之类,函数调用对应call
,返回对应于jmpl
。
对这些平台来说,只要掌握上面的指令,就可以在函数里,把几万行的汇编代码分成一小块一小块来分析,每小块的其它指令查手册就行了。
本人的coredump
系列第四章也是这个思路,详情请见开发目录
剩余内容请看本人公众号debugeeker, 链接为如何半天学会一门汇编