如何半天学会一门汇编
2022-12-4 11:47:11 Author: 奶牛安全(查看原文) 阅读量:7 收藏


本文讲述如何用半天时间学会一门汇编的诀窍。在学习汇编过程,最好用Visual Studio调试,打开汇编模式,把栈视图和寄存器视图都打开。函数调用使用cdecl,在调试过程中使用汇编单步。

很多程序员都觉得汇编是可怕的编程语言,感觉很难学,繁多的指令,各种寄存器,寻址方式和CPU机制紧密相关,一切都让人望而却步。其实,汇编相对众多编程语言来说,是一门非常简单的语言:它没有奇技淫巧式的语法,也没有各种全家桶式的框架。它之所以显得非常难掌握的原因:

  1. 它解决的问题,离程序员平时面临的问题太远。
  2. 目前很多编程语言书籍和资料都是集中该语言本身,很少会和其它语言横向对比和建立联系。讲C语言就是讲C语言,讲C++也只是讲C++,讲汇编也是只是讲汇编。至于C/C++和汇编之间的对比和联系呢?没有。

但在现实生活中,还是有不少地方用到汇编语言,除了搞嵌入式之外,在C/C++,OC之类的语言,在定位程序崩溃,内存泄露,逆向破解,漏洞挖掘和分析,恶意软件分析,都会用到。

所以,还是需要学一下汇编的。如何学呢?重要是把它和程序员平时面临的问题和熟识的语言建立一种联系。这和学数理化差不多,数理化学得好的人,基本上都会把抽象思维和现实世界建立某种联系。

在所有的编程语言中,这三样东西基本上是不可或缺的:

  1. 函数
  2. 程序执行顺序
  3. 数据结构

所以,重要是建立这三样东西在高级编程语言C/C++和汇编的对应关系。对于什么指令,寄存器,寻址方式和CPU机制,就先不管。

函数

在高级编程语言里,函数的参数传递是通过变量或数值,返回值是通过变量或数值。那么在汇编里呢?在汇编里,参数传递和返回结果叫做调用约定

调用约定传递参数和返回值,是通过寄存器和栈来完成的。那么,就要看传递参数用到什么寄存器,返回值用到什么样的寄存器。由于寄存器数量有限,就演变成这些问题:

  1. 第一二三四五...这些参数分别用哪些寄存器来传递?有没有个数限制,超过了限制,参数又如何传递?
  2. 返回值通过哪个寄存器传递?
  3. 如果通过栈来传递,标志栈的是哪个寄存器?
  4. 在C++的情况下,成员函数的参数传递又是如何?
  5. 当前函数桢用哪个寄存器表示?
  6. 函数执行完,如何返回调用者?

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平台,其中x86sparc是08-10年时,mips是11年-12年接触的。iOS是在2020年搞了一天,只是为了看看jailbreak反检测机制。

从上面来看,可以需要了解的寄存器少了很多,而且需要了解的寄存器都是有关联的。而且这些知识点可以这样掌握:

  1. 编写没有参数和返回值的函数,只是进行简单的1+1的操作。了解一下编译器会生成哪些汇编
  2. 编写没有参数有返回值的函数,return 1+1的操作,了解返回值是放在哪个寄存器的。
  3. 编写有参数有返回值的函数,了解一下参数是如何传递,并且把参数的个数不断增加,看看传递改变。
  4. 编写一个类和一个成员函数,看看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系列第四章也是这个思路,详情请见开发目录

数据结构

从上面可以看到要学习汇编的思路:无非先把高级语言的函数和汇编的函数对应起来,再把高级语言函数里的执行顺序和汇编的指令对应起来,把函数分成一小块一小块代码段来分析。那么,对这些代码段的指令,基本查手册就行了。

那么,这些代码段具体做了什么事呢?或者说,操作了哪些数据呢?因为只是从汇编的角度来说,很多时候只能管中窥豹,看了之后不明其然,根本不知道是操作什么数据。

所以,就需要看一下各种数据结构在汇编里的操作特征。基本上,C/C++语言本身的数据结构也只是如下几种:

  1. char
  2. short
  3. int
  4. long
  5. float
  6. double
  7. 数组
  8. 结构体
  9. 联合体
  10. 指针
  11. 函数指针
  12. 虚函数表

编写程序分别操作这些数据结构类型,看看它们在汇编里的表现是如何。只要掌握了它们的特征,那么即使只看汇编代码,也知道是在处理什么类型的数据。

本人coredump系列的第五,六章就是这种思路,详情请见开发目录

暗号:9138e


文章来源: http://mp.weixin.qq.com/s?__biz=MzU4NjY0NTExNA==&mid=2247487126&idx=1&sn=d862026993094174d0994feb3403c1bf&chksm=fdf96583ca8eec95abaee24328600ae52a04417c8bfe278ef77823c859388afc8db31c0c9831#rd
如有侵权请联系:admin#unsafe.sh