汇编学习笔记

汇编学习笔记

花了一周的时间,对汇编做了一次深刻的复习和再学习,想记下来的东西有很多,我尽量把总结写全。

0x01.8086汇编

个人认为学习汇编从8086入手对之后的汇编理解有非常大的帮助,这时候很想推荐一本书,王爽的《汇编语言》,深入浅出地讲解了8086汇编,我自己也是从头刷了一遍,对汇编的世界有了更清楚的认识。总的来说,我认为入门汇编要学的东西,真的不多,因为所有的函数调用来来回回都是这么点东西,接下来我会做一些总结

0x011.总线

image.png

总线分为地址总线、数据总线和控制总线,上图是CPU从内存中读取数据的过程,它们很形象地反映了关于他们的各自的职责。当然每根线都他们各自的算法,这里不展开,网上有很多基础资料。

0x012.寄存器

我们都知道当CPU做大部分运算的时候,都是将内存的值读到CPU中,就是存在我们所说的寄存器中,再在CPU中进行加减乘除与或非这些基本的逻辑运算。所有的程序都是基于这成千上万次上述的操作。

8086CPU有14个寄存器:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW都是16位。这其中又分为通用寄存器和段寄存器、标志寄存器等。

AX、BX、CX、DX这四个被称为通过寄存器,他们主要是存储数据的,因为是16位的,所有只有"可怜"的2字节可以存。这其中又分为高八位和低八位,主要是为了空间的合理利用。

而段寄存器包括CS(Code segment)和IP(偏移)合成指令的物理地址、DS(Data segment)存放要访问数据的段地址、SS(Stack segment)和SP(偏移)。另外一个BP寄存器一般用来存放栈的基址来配合操作栈

标志寄存器(flag)记录当前指令结果的状态。

零标志位(Zero Flag)如果结果为0,那么zf = 1(表示结果是0);如果结果不为0,那么zf = 0。

奇偶标志位 (PF) ,如果1的个数为偶数,pf = 1,如果为奇数,那么pf = 0。

符号标志位(Symbol Flag),记录其结果是否为负。如果结果为负,sf = 1;如果非负,sf = 0。

进位标志位(CF)在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。

溢出标志位(OF),记录了有符号数运算的结果是否发生了溢出。如果发生溢出,OF = 1;如果没有,OF = 0。

0x013.栈

image.png

栈的开口一般是向上的,栈底为高地址,栈顶为低地址,开栈的操作是从高地址往低地址走。

总的来说,上图红线间隔区域就是一个函数调用栈也称为一个栈帧,即使是现在方法调用的实现原理也依然是大同小异。在一个栈帧当中,包含了函数所需要的环境。比如参数、局部变量、以及返回地址(回家的路)。函数和函数之间就是这样一段接着一段进行执行。首先参数入栈,该参数是下个函数调用需要使用的参数,然后记录方法下一条的执行地址,方便方法返回后继续执行,之后用BP存一下栈帧的基地址和拉伸SP开启栈帧内部的域,之后在函数体内的局部变量都存在这个栈帧中,如果还有下一个方法那么继续入参和保存返回地址重复上述操作。如果方法调用完毕,所有的环境都会被POP完,回到上一个函数继续执行,所以这也就是为什么局部变量过了方法,再访问就会出现很大的危险,因为该域已经是一个释放掉的域。

PS:在开发过程中经常碰到Stack over flow(栈溢出)就是因为开发者不停地在开栈帧,却得不到释放,栈的空间被用光了,比如你写了一个没有边界限制的递归,反正我日常开发能不写递归就不写,如果写递归我会尤其要注意边界的条件,及时return。我一直觉得递归本身效率就不高,只是代码看起来易读、易理解罢了。

0x014.栈平衡

栈平衡又分为外平栈和内平栈,其实没有什么特别的东西,就是当函数调用完毕之后需要把SP拉回来,否则栈迟早会被用完、因为所有的开栈操作都是通过SP为基址进行的。内平栈就是在栈帧内部做平衡,外平栈就是函数调用完进行平栈。

0x02.ARM64

之后来到现代社会,看看如今的arm64和8086的区别

0x021.寄存器

ARM64也拥有有31个64位的通用寄存器 x0 到 x30,他们也是通常用来存储数据,w0 到 w28 这些是32位的. 因为64位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.比如 w0 就是 x0的低32位!

PC寄存器(program counter)为指令指针寄存器,它指示了CPU当前要读取指令的地址

浮点寄存器 64位: D0 - D31 32位: S0 - S31,向量寄存器 128位:V0-V31

SP寄存器其实是X31寄存器,他在任意时刻会保存我们栈顶的地址.(是不是和8086的SP一模一样)

FP寄存器也称为x29寄存器属于通用寄存器,一般我们利用它保存栈底的地址(是不是和8086的BP很像)

x30寄存器也称为LR寄存器,存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!

ARM64同样有状态寄存器,这和8086大同小异。

0x022.高速缓存

CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU开始有了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成).CPU直接从高速缓存依次读取指令来执行,iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M。

0x023.关于内存读写指令

注意:读/写 数据是都是往高地址读/写

str(store register)指令

将数据从寄存器中读出来,存到内存中.

ldr(load register)指令

将数据从内存中读出来,存到寄存器中

此ldr 和 str 的变种ldp 和 stp 还可以操作2个寄存器.

0x024.函数调用栈

关于函数调用时对栈的处理其实基本是和8086差不多,只要懂了8086汇编,理解起ARM64函数调用就非常容易了,只是在规则上略有不同。

  • 一般来说 arm64上 x0 - x7 分别会存放方法的前 8 个参数。
  • 如果参数个数超过了8个,多余的参数会存在栈上,新方法会通过栈来读取。因此少量的参数可以帮助我们提升性能,CPU计算,肯定比先读内存再计算来的快。
  • 方法的返回值一般都在 x0 上。
  • 如果方法返回值是一个较大的数据结构时,结果会存在 x8 执行的地址上。

0x03.思考与感悟

下面聊一下反思与感悟。

0x031.ARM64在进行栈拉伸的时候到底是如何计算的

开始的时候我一直很不理解的一件事就是:它是怎么知道每个栈需要开辟多少空间的?比如我们在方法内部用到的局部变量一多,他就会开辟多一些的栈空间(因为需要字节对齐的缘故,基本是以0x10、0x20、0x30..规则拉伸),当入参变多时,他同样会开辟多一些的空间,即使你在内部没有用到,还有当遇到可变参数时,他的规则又是怎么样的。找了很久,终于在公司内网找到了汇编大牛指引了方向。其实在arm的开发者官网上都有说明 https://developer.arm.com/documentation/ihi0055/latest/ ,英文不好的同学可以查看译文arm64程序调用规则

总而言之,言而总之。对于调用者而言,即使他认为在调用之前已经分配了足够的堆栈空间来容纳的参数,但实际上,只有在参数封装处理之后才能知道所需的堆栈空间量到底需要多少。(因为会牵涉到指针变量、结构体、可变参数等等)。因此参数传递过程大致分为3个阶段:

  • 阶段A – 初始化 (在开始处理参数之前,该阶段仅执行一次)

    1. NGRN = 0
    2. NSRN = 0
    3. NSAA = SP(NSAA设置为当前的SP)
  • 阶段B - 预填充和扩展参数 (把参数列表中的每一个参数,去匹配下面规则,第一个被匹配到的规则,应用到该参数上。如果没有规匹配到规则,那么参数不修改)

    1. 如果参数类型是复合类型,调用者和被调用者都不能确定其大小,则将参数复制到内存中,并将参数替换为指向该内存的指针。 (C / C ++语言中没有这样的类型,其它语言存在。)
    2. 如果参数是HFA或HVA类型,则参数不修改。
    3. 如果参数是大于16个字节的复合类型,调用者申请一个内存,将参数复制到内存里去,并将参数替换为指向该内存的指针。
    4. 如果参数是复合类型,则参数的大小向上舍入为最接近8个字节的倍数。(例如参数大小为9字节,修改为16字节)
  • 阶段C- 把参数放到寄存器或栈里 (参数列表中的每个参数,将依次应用以下规则,直到参数放到寄存器或栈里,此参数处理完成,然后再从参数列表中取参数。注: 将参数分配给寄存器时,寄存器中未使用的位的值不确定。 将参数分配给栈时,未填充字节的值不确定。)

    1. (1) 如果参数是half(16bit),single(16bit),double(32bit)或quad(64bit)浮点数或Short Vector Type,并且NSRN小于8,则将参数放入寄存器v[NSRN]的最低有效位。 NSRN增加1。 此参数处理完成。
    2. (2) 如果参数是HFA(homogeneous floating-point aggregate)或HVA(homogeneous short vector aggregate)类型,且NSRN + (HFA或HVA成员个数) ≤ 8,则每个成员依次放入SIMD and Floating-point 寄存器,NSRN=NSRN+ HFA或HVA成员个数。此参数处理完成。
    3. (3) 如果参数是HFA(homogeneous floating-point aggregate)或HVA(homogeneous short vector aggregate)类型,但是NSRN已经等于8(说明v0-v7被使用完毕)。则参数的大小向上舍入为最接近8个字节的倍数。(例如参数大小为9字节,修改为16字节)
    4. (4) 如果参数是HFA(homogeneous floating-point aggregate)、HVA(homogeneous short vector aggregate)、quad(64bit)浮点数或Short Vector Type,NSAA = NSAA+max(8, 参数自然对齐大小)。
    5. (5) 如果参数是half(16bit),single(16bit)浮点数,参数扩展到8字节(放入最低有效位,其余bits值不确定)
    6. (6) 如果参数是HFA(homogeneous floating-point aggregate)、HVA(homogeneous short vector aggregate)、half(16bit),single(16bit),double(32bit)或quad(64bit)浮点数或Short Vector Type,参数copy到内存,NSAA=NSAA+size(参数)。此参数处理完成。
    7. (7) 如果参数是整型或指针类型、size(参数)<=8字节,且NGRN小于8,则参数复制到x[NGRN]中的最低有效位。 NGRN增加1。 此参数处理完成。
    8. (8) 如果参数对齐后16字节,NGRN向上取偶数。(例如:NGRN为2,那值保持不变;假如NGRN为3,则取4。 注:iOS ABI没有这个规则)
    9. (9) 如果参数是整型,对齐后16字节,且NGRN小于7,则把参数复制到x[NGRN] 和 x[NGRN+1],x[NGRN]是低位。NGRN = NGRN + 2。 此参数处理完成。
    10. (10) 如果参数是复合类型,且参数可以完全放进x寄存器(8-NGRN>= 参数字节大小/8)。从x[NGRN]依次放入参数(低位开始)。未填充的bits的值不确定。NGRN = NGRN + 此参数用掉的寄存器个数。此参数处理完成。
    11. (11) NGRN设为8。
    12. (12) NSAA = NSAA+max(8, 参数自然对齐大小)。
    13. (13) 如果参数是复合类型,参数copy到内存,NSAA=NSAA+size(参数)。此参数处理完成。
    14. (14) 如果参数小于8字节,参数设置为8字节大小,高位bits值不确定。
    15. (15) 参数copy到内存,NSAA=NSAA+size(参数)。此参数处理完成。

从上面规则,可以得到经验:

  1. 处理完参数列表中所有的参数后,调用者通过特定的规则一定知道传递参数用了多少栈空间。(NSAA - SP)
  2. 浮点数和short vector types通过v寄存器和栈传递,不会通过r寄存器传递。(除非是小复合类型的成员)
  3. 寄存器和栈中,参数未填充满的部分的值,不可确定。

0x032.AT&T汇编和 Intel汇编的区别

AT&T 就是我们在开发的ARM系列的汇编,Intel汇编就是我们说的x86汇编,其实在实现思路上,他们大致还是相同的。只是在指令集和寄存器的设计上略有不同。

image.png

所以Xcode模拟器(Intel)和我们的真机调试(ARM)还是有一些略微不同,在调试的时候也要略微的注意语法。比如MOV的赋值操作,操作数和被操作数需要反一反。

0x033.msgSend的Hook

有了之前的基础之后,我们可以用汇编来做一些很trick的事,比如在iOS的开发过程中,我们可以hook objc的msgSend进行一些耗时方法的统计。其实msgSend本身是可以用fishhook去做hook的,我们知道像msgsend这类的系统函数,是需要在启动时动态绑定的(和NSlog一样),因此可以成功hook。难点在于,msgSend的入参是可变参数,还有是否有返回值、返回值类型等诸多问题,所以用常见的开发语言是无法实现此类的函数的,所以他的实现本身就是用汇编实现的,不能当简单的函数hook来处理。但是要强行hook还是能做到的,github也已经有较现成的方法:https://github.com/czqasngit/objc_msgSend_hook,在这里讲一下作者大致的思路。

1.首先使用fishhook去hook msgsend,用hook_objc_msgSend去替换origin_objc_msgSend

2.在hook_objc_msgSend第一步首先我们需要保存函数的环境,所以将x0-x7依次入栈。

3.这时候作者是开始执行了自己的函数(beforeCall),比如当前时间打点,保存类名信息等等,之后通过线程私有共享缓存进行保存。

4.依次将寄存器数据出栈,恢复环境,进行原始方法origin_objc_msgSend的调用。

5.重复2,3的操作,执行自己的函数(afterCall),比如记录方法的耗时操作等等。

6.调用ret,继续执行X30中的函数地址,让程序继续执行下去。

以上就是该作者的大致思路,实现上确实不难,能想到怎么实现才是真的难。

类似这样的实现思路还能做很多事,比如hook全局的block,思路差不多,先用fishhook hook系统函数,然后用汇编处理函数环境 比如这位大佬 https://github.com/youngsoft/YSBlockHook

0x04.总结

通过本次的学习,对汇编的实现有了新的认识,也学到了用汇编去帮助我们去实现一些技术方案,收获也是颇丰。其实当我们深入去看日常的汇编代码的时候,我们会发现日常接触到的所有函数,搞来搞去就这么点操作,入栈出栈、存储读取等等,只要熟练掌握规则,其实看汇编真的只是烦,但他不难!

五一颓废了,5天基本都在玩,没学习,挤出这么点学习成果自我安慰下。

0x05.引用

arm64程序调用规则

https://github.com/czqasngit/objc_msgSend_hook

https://github.com/youngsoft/YSBlockHook

https://developer.arm.com/documentation/ihi0055/latest/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342