阳高县网站建设,南京律师网站建设,扬州住房和建设局网站,在北京注册个公司要多少钱经过前面几节课的学习#xff0c;我们在一些地方都会使用汇编来分析#xff0c;我们学习汇编#xff0c;只是学习一些基础#xff0c;主要是在我们需要深入分析语法的时候#xff0c;使用汇编分析#xff0c;这样会让我们更熟悉c编译器和语法。
从这节课开始#xff0c…经过前面几节课的学习我们在一些地方都会使用汇编来分析我们学习汇编只是学习一些基础主要是在我们需要深入分析语法的时候使用汇编分析这样会让我们更熟悉c编译器和语法。
从这节课开始会持续加餐争取把类对象模型分析出来。好了不多说了我们继续。
C学习b站直播视频 文章目录 5.1 x86汇编介绍5.1.1 Intel汇编5.1.2 ATT汇编 5.2 x86寄存器介绍5.2.1 Intel寄存器解读5.2.2 Intel对数据的描述5.2.3 寄存器作用描述 5.3 x86常用指令5.3.1 mov 传送指令5.3.2 pushpop5.3.3 lea取地址5.3.4 跳转语句5.3.5 函数调用返回5.3.6 算术运算5.3.7 位运算指令5.3.8 附加rep stos 5.4 ATT汇编分析5.5 Intel vs ATT5.5.1 操作数长度5.5.2 寄存器命名5.5.3 立即数5.5.4 操作数的方向5.5.5 内存单元操作数5.5.6 间接寻址方式5.5.7 总结 5.6 函数调用详解5.6.1 函数参数约定5.6.1.1 Intel方式5.6.1.2 ATT方式 5.6.2 函数的栈帧5.6.2.1 Intel方式5.6.2.2 ATT方式 5.6.3 函数返回值5.6.3.1 Intel方式5.6.3.2 ATT方式 5.6.4 函数退出5.6.4.1 Intel方式5.6.4.2 ATT方式 5.7 扩展ARM平台汇编分析5.7.1 RISC与CISC的区别5.7.2 ARM寄存器解读5.7.3 ARM对数据的描述5.7.4 ARM汇编浅析 5.1 x86汇编介绍
5.1.1 Intel汇编
Windows派系的汇编代码VS就是我们一直用的这个IDE就是这种风格的。不知道clang是什么风格clang占的内存较大我就不安装了有条件的同学可以自行测试。
后面几节课都是使用vs2022来分析的所以这种汇编风格我们是需要学习的。可以先来看看
int func(int i)
{
00007FF72DF617E0 89 4C 24 08 mov dword ptr [rsp8],ecx
00007FF72DF617E4 55 push rbp
00007FF72DF617E5 57 push rdi
00007FF72DF617E6 48 81 EC E8 00 00 00 sub rsp,0E8h
00007FF72DF617ED 48 8D 6C 24 20 lea rbp,[rsp20h]
00007FF72DF617F2 48 8D 0D 6E F8 00 00 lea rcx,[00007FF72DF71067h]
00007FF72DF617F9 E8 54 FB FF FF call 00007FF72DF61352 return 2 * i;
00007FF72DF617FE 8B 85 E0 00 00 00 mov eax,dword ptr [rbp00000000000000E0h]
00007FF72DF61804 D1 E0 shl eax,1
}
00007FF72DF61806 48 8D A5 C8 00 00 00 lea rsp,[rbp00000000000000C8h]
00007FF72DF6180D 5F pop rdi
00007FF72DF6180E 5D pop rbp
00007FF72DF6180F C3 ret int main()
{
00007FF72DF61830 40 55 push rbp
00007FF72DF61832 57 push rdi
00007FF72DF61833 48 81 EC 08 01 00 00 sub rsp,108h
00007FF72DF6183A 48 8D 6C 24 20 lea rbp,[rsp20h]
00007FF72DF6183F 48 8D 0D 21 F8 00 00 lea rcx,[00007FF72DF71067h]
00007FF72DF61846 E8 07 FB FF FF call 00007FF72DF61352 // std::cout Hello World!\n;int s func(255);
00007FF72DF6184B B9 FF 00 00 00 mov ecx,0FFh
00007FF72DF61850 E8 82 F8 FF FF call 00007FF72DF610D7
00007FF72DF61855 89 45 04 mov dword ptr [rbp4],eax return 0;
00007FF72DF61858 33 C0 xor eax,eax
}
00007FF72DF6185A 48 8D A5 E8 00 00 00 lea rsp,[rbp00000000000000E8h]
00007FF72DF61861 5F pop rdi
00007FF72DF61862 5D pop rbp
00007FF72DF61863 C3 ret 5.1.2 ATT汇编
ATT汇编读作“AT and T”是 American Telephone Telegraph 的缩写是在贝尔实验室多年运营。
主要使用的地方就是GCCObjdump。这个我们学linux就知道了。
主要使用的平台Linux、Unix、Max OS、iOS模拟器
同一份代码我们在linux下看看
0000000000401146 _Z4funci:401146: 55 push %rbp401147: 48 89 e5 mov %rsp,%rbp40114a: 89 7d fc mov %edi,-0x4(%rbp)40114d: 8b 45 fc mov -0x4(%rbp),%eax401150: 01 c0 add %eax,%eax401152: 5d pop %rbp401153: c3 retq 0000000000401154 main:401154: 55 push %rbp401155: 48 89 e5 mov %rsp,%rbp401158: 48 83 ec 10 sub $0x10,%rsp40115c: bf ff 00 00 00 mov $0xff,%edi401161: e8 e0 ff ff ff callq 401146 _Z4funci401166: 89 45 fc mov %eax,-0x4(%rbp)401169: b8 00 00 00 00 mov $0x0,%eax40116e: c9 leaveq 40116f: c3 retq 感觉还是g的风格简单些。
5.2 x86寄存器介绍 这个图来自《深入理解计算机系统》。
这些寄存器都是x86体系下的寄存器当然x86的寄存器还有很多我下载过intel的芯片手册都是英文看的头都大了。我们只要了解这些就可以了。当然x86还有存浮点数的寄存器这个没有了解就不说上面这个图才是我们重点。注意最后面注释介绍的是ATT。
5.2.1 Intel寄存器解读
Intel的寄存器有一个很有意思的地方就是因为要兼容以前的所以寄存器的位数从8位到16位到32位再到64位都有我们看上面第一个寄存器RAX
64位叫rax
32位叫eax
16位叫ax
8位叫al
所以我们待会看汇编的时候就会看到不过一般会看到rax和eax现在16位和8位已经被淘汰了。
5.2.2 Intel对数据的描述
Intel作为业界的老大哥背负的历史包袱过重哈哈哈。
c声明Intel数据类型char字节short字int双字long四字
总感觉当年16位的时候觉得够大了吧所以叫字然后int出来之后懵逼了只能叫双字这个其实不用记只是简单介绍一下。到时候可以看看ARM没有历史负担就是任性
5.2.3 寄存器作用描述
为啥是简单描述是因为两种汇编风格对函数调用的传参不一样这就有点难搞哈哈哈。 图片引用了https://blog.csdn.net/c2682736/article/details/122349851这个链接的。
特殊用处寄存器介绍
rax函数返回值
rdx在Intel方式中做第二个参数在ATT中做第三个参数甚至还可以做第二个返回值寄存器。
rsp栈顶指针linux是满减栈看着windows是往上加的栈
rbp栈底指针两个指针一减就可以算出栈大小。
rip指令寄存器
函数参数寄存器这个因为两种方式不一样所以在后面再详解。
5.3 x86常用指令
上面介绍了寄存器接着我们介绍一下常用指令指令寄存器的操作就相当于我们的一条语句。不过这个指令不需要记也记不住说实话看到不懂再查询。
5.3.1 mov 传送指令
// 我们先开始看mov命令这个命令是出常见的也是我们c使用的赋值语句。int s 1;
00007FF72F8E562D C7 45 04 01 00 00 00 mov dword ptr [rbp4],1 // rbp是栈基地址rbp4就是s的地址mov就是把1赋值到[rbp4] (dword就是双字)// []是对这个地址寻址也就是*(rbp4)的意思int s1 s;
00007FF72F8E5634 8B 45 04 mov eax,dword ptr [rbp4] // 这是两个变量赋值两个变量赋值需要借助寄存器了eax.// 第一次先把[rbp4]赋值eax寄存器
00007FF72F8E5637 89 45 24 mov dword ptr [rbp24h],eax// 第二次再把这个寄存器eax赋值到[rbp24h][rbp24]就是s1的地址int* ps s;
00007FF72F8E563A 48 8D 45 04 lea rax,[rbp4] // lea是我们后面介绍的取地址指令把[rbp4]的地址赋值到rax中
00007FF72F8E563E 48 89 45 48 mov qword ptr [rbp48h],rax // 然后在把rax的值赋值到[rbp48h]中(qword是四字指针在64位系统都是8字节)*ps 111;
00007FF72F8E5642 48 8B 45 48 mov rax,qword ptr [rbp48h] // 把ps的值赋值到rax因为ps是指针所以这个值其实就是地址
00007FF72F8E5646 C7 00 6F 00 00 00 mov dword ptr [rax],6Fh // 然后对rax就是指向的内存就行赋值[]就是寻址的意思5.3.2 pushpop
这两个命令一起来很明显push是压栈pop是出栈只有调用函数才有这个操作。
我们来看看
int test()
{
00007FF7486817D0 40 55 push rbp // 这是把上一个函数的栈基地址保存到栈里具体的函数栈帧我们后面分析
00007FF7486817D2 57 push rdi // rdi没记错应该是第一个参数我也不清楚为啥要入栈
00007FF7486817D3 48 81 EC E8 00 00 00 sub rsp,0E8h // 这句也是我疑惑的地方不是不知道这句意思是不知道为啥windos栈能隔这么大。// 这句意思就是把栈指针往下减0E8h这么大空间
00007FF7486817DA 48 8D 6C 24 20 lea rbp,[rsp20h]// 然后在把rsp往上加20h作为这个函数的栈空间这个我试过了如果变量过大这个空间也会变大等下我们试试
00007FF7486817DF 48 8D 0D 81 F8 00 00 lea rcx,[00007FF748691067h]
00007FF7486817E6 E8 67 FB FF FF call 00007FF748681352 // 后面这两句我更疑惑了调用了一个函数就不分析这个函数干啥了。return 0;
00007FF7486817EB 33 C0 xor eax,eax // xor是后面的指令意思就是异或反正两个都是0
}
00007FF7486817ED 48 8D A5 C8 00 00 00 lea rsp,[rbp00000000000000C8h] // rbpC8h其实就是等于rsp原来指向的地址因为上面rbprsp20h这个20hC8hE8h就是当初rsp减少的空间
00007FF7486817F4 5F pop rdi // 然后再把rdi从栈中弹出到rdi寄存器里
00007FF7486817F5 5D pop rbp // 上面把rsp还原了这里肯定要把rbp也还原
00007FF7486817F6 C3 ret 5.3.3 lea取地址
这个上面说了就不继续说用处就是取地址。
5.3.4 跳转语句 int s2 0;
00007FF650C34671 C7 45 64 00 00 00 00 mov dword ptr [rbp64h],0 if (s 1)
00007FF650C34678 83 7D 04 01 cmp dword ptr [rbp4],1 // cmp就是比较语句比较这两个值是否想等// 如果operand1等于operand2则ZF被置为1否则ZF被置为0。如果operand1小于operand2则SF被置为1否则SF被置为0。如果操作过程中有进位则CF被置为1否则CF被置为0。
00007FF650C3467C 75 07 jne 00007FF650C34685 // jne是判断上一条语句不等于0即ZF标志位为0两个不想等ZF为0才跳转// 还有其他跳转命令je jle jl jg等等{s2 1;
00007FF650C3467E C7 45 64 01 00 00 00 mov dword ptr [rbp64h],1 }s2 2;
00007FF650C34685 C7 45 64 02 00 00 00 mov dword ptr [rbp64h],2 // 后面就不解释了5.3.5 函数调用返回 // 测试push poptest();
00007FF650C3466C E8 54 CD FF FF call 00007FF650C313C5 call 函数调用ret 返回这也比较常见了。
5.3.6 算术运算
add 想加sub 相减inc 加1mul 相乘div 相除 int s4 s3 4;
00007FF63E6F4C66 8B 85 84 00 00 00 mov eax,dword ptr [rbp0000000000000084h]
00007FF63E6F4C6C 83 C0 04 add eax,4 其他的就不演示了。
5.3.7 位运算指令
and 与运算or 或运算xor 异或运算not 取反
不演示了。
5.3.8 附加rep stos int s5[20] { 0 };
00007FF6BD944C75 48 8D 85 D0 00 00 00 lea rax,[rbp00000000000000D0h] // 把数组的地址赋值到rax中
00007FF6BD944C7C 48 8B F8 mov rdi,rax // 然后在赋值到rdi中
00007FF6BD944C7F 33 C0 xor eax,eax // 然后把eax清0
00007FF6BD944C81 B9 50 00 00 00 mov ecx,50h // 50h是这个数组的大小
00007FF6BD944C86 F3 AA rep stos byte ptr [rdi] 这个还挺常用的用在一个结构体或类或数组一些符合数据结构置0的时候。
后面的rep stos是我们重点这其实是两个指令复合使用。
REPRepeat汇编指令是一种重复执行指令的控制指令它通常和其他指令组合使用用于在处理字符串或数组时进行重复操作。
REP指令可以和多个其他指令搭配使用如MOV、STOS等。其语法如下
rep instruction
其中instruction是要重复执行的指令。REP指令会将ECX寄存器中的值作为计数器循环执行instruction指定的操作。每次循环都会将ECX减1直到ECX的值为0为止。
还记得刚刚ecx赋值了50h
STOS指令将AL/AX/EAX的值存储到[EDI]指定的内存单元
STOS指令使用AL字节 - STOSBAX字 - STOSW或EAX对于双 - STOSD的数据复制目标字符串在内存中通过ESDI指向。
我们这个数组是int是双子所以使用eax我们前面不是刚用eax异或就是为了清0.
所以这个句话就是把数组清0了。
5.4 ATT汇编分析
上面我们是对windos下的Intel汇编格式的分析下面我们就分析linux下的ATT汇编格式这里就不在区分了我们直接看了看完了我们下一节就可以总结两种方式的差异。
// 编译是需要加-g,objdump -S 才能反汇编出带源码
0000000000401146 _Z4testv:#include iostreamint test()
{401146: 55 push %rbp401147: 48 89 e5 mov %rsp,%rbpreturn 0;40114a: b8 00 00 00 00 mov $0x0,%eax
}40114f: 5d pop %rbp401150: c3 retq // retq就是退出// linux的函数风格我比较喜欢因为比较简单不像windos那么复杂0000000000401151 main:int main()
{401151: 55 push %rbp// 把栈基地址入栈寄存器是用%401152: 48 89 e5 mov %rsp,%rbp// 把新的rsp地址移动到rbp,ATT的mov源操作数是在中间401155: 48 83 ec 70 sub $0x70,%rsp// 把rsp栈的大小设置为0x70h这么大int s 1;401159: c7 45 e0 01 00 00 00 movl $0x1,-0x20(%rbp)// l其实就是intrbp-20h就是s的地址原来这就是满减栈的意思window是栈是往上加int s1 s;401160: 8b 45 e0 mov -0x20(%rbp),%eax// 如果不看源码这个确实不好理解就是把rbp-20h的值赋值给eax401163: 89 45 fc mov %eax,-0x4(%rbp)// 然后再把eax的值赋值到s1,这里有点奇怪为啥s1的地址是rbp-4h??int* ps s;401166: 48 8d 45 e0 lea -0x20(%rbp),%rax// lea取地址熟悉不就是把s的地址赋值到rax中40116a: 48 89 45 f0 mov %rax,-0x10(%rbp)// 然后再在rax赋值到ps里这里ps的地址是rbp-10h*ps 111;40116e: 48 8b 45 f0 mov -0x10(%rbp),%rax401172: c7 00 6f 00 00 00 movl $0x6f,(%rax)// 这个也是赋值这个()也是寻址意思跟Intel的[]是一个意思// 测试push poptest();401178: e8 c9 ff ff ff callq 401146 _Z4testv// 调用也是用call不过加了个后缀int s2 0;40117d: c7 45 ec 00 00 00 00 movl $0x0,-0x14(%rbp)if (s 1)401184: 8b 45 e0 mov -0x20(%rbp),%eax401187: 83 f8 01 cmp $0x1,%eax40118a: 75 07 jne 401193 main0x42// 这个cmp和jne就一样了{s2 1;40118c: c7 45 ec 01 00 00 00 movl $0x1,-0x14(%rbp)}s2 2;401193: c7 45 ec 02 00 00 00 movl $0x2,-0x14(%rbp)int s3 1 4;40119a: c7 45 e8 05 00 00 00 movl $0x5,-0x18(%rbp)int s4 s3 4;4011a1: 8b 45 e8 mov -0x18(%rbp),%eax4011a4: 83 c0 04 add $0x4,%eax4011a7: 89 45 e4 mov %eax,-0x1c(%rbp)// add也是一样的int s5[20] { 0 };4011aa: 48 c7 45 90 00 00 00 movq $0x0,-0x70(%rbp)4011b1: 00 4011b2: 48 c7 45 98 00 00 00 movq $0x0,-0x68(%rbp)4011b9: 00 4011ba: 48 c7 45 a0 00 00 00 movq $0x0,-0x60(%rbp)4011c1: 00 4011c2: 48 c7 45 a8 00 00 00 movq $0x0,-0x58(%rbp)4011c9: 00 4011ca: 48 c7 45 b0 00 00 00 movq $0x0,-0x50(%rbp)4011d1: 00 4011d2: 48 c7 45 b8 00 00 00 movq $0x0,-0x48(%rbp)4011d9: 00 4011da: 48 c7 45 c0 00 00 00 movq $0x0,-0x40(%rbp)4011e1: 00 4011e2: 48 c7 45 c8 00 00 00 movq $0x0,-0x38(%rbp)4011e9: 00 4011ea: 48 c7 45 d0 00 00 00 movq $0x0,-0x30(%rbp)4011f1: 00 4011f2: 48 c7 45 d8 00 00 00 movq $0x0,-0x28(%rbp)4011f9: 00 // 看来linux下没有rep stos这个命令在使用笨方式一个一个清0return 0;4011fa: b8 00 00 00 00 mov $0x0,%eax
}4011ff: c9 leaveq 401200: c3 retq 5.5 Intel vs ATT
上面我们分析过了Intel和ATT的汇编然后我们来总结一下这两种汇编的区别。
5.5.1 操作数长度
我们操作一个数的大小其实可以通过在汇编指令上体现出来的。
ATT的操作码后面有一个后缀其含义就是操作码的大小。
(后缀分别为b、w、l、q)
在Intel的语法中则要在内存单元操作数的前面加上前缀。
(前缀分别为byte ptr、world ptr、dworld ptr、qworld ptr)
c声明Intel数据类型Intel语法ATT语法大小char字节mov al,blmovb %bl,%al1short字mov ax,bxmovw %bx,%ax2int双字mov eax,dworld ptr[ebx]movl (%ebx),%eax4long四字mov rax, qworld ptr[rbx]movq (%rbx),%rax8float单精度不知道后缀s4char *四字不知道后缀q8double双精度不知道后缀l8
我对汇编指令操作浮点确实不熟悉因为我们自己分析的大部分都是整数以后如果碰到了操作浮点的再回来补充补充。
5.5.2 寄存器命名
通过上面的例子应该都发现了寄存器命名两种方式的汇编不一样。
在Intel的语法中寄存器是不需要前缀的。
在ATT的语法中寄存器是需要加%前缀
Intel语法ATT语法mov rax, qworld ptr[rbx]movq (%rbx),%rax
5.5.3 立即数
立即数的写法也不一样。
在Intel语法中十六进制和二进制立即数后面分别冠以h和b
在ATT语法中立即数前面加上$然后如果是十六进制前面再加上0x
Intel语法ATT语法mov eax,8movl **$**8,%eaxmov ebx,0ffffhmovl **$**0xffff,%ebx
5.5.4 操作数的方向
这一点我就好奇为啥搞的方向相反这样看起代码真的难受。
Intel与ATT操作数的方式正好相反。
在Intel语法中第一个操作数是目的操作数第二个操作数是源操作数
在ATT中第一个数是源操作数第二个数是目的操作数
Intel语法ATT语法mov eax,[ecx]movl (%ecx),%eax
5.5.5 内存单元操作数
从上面例子可以看出内存操作数也有所不同。
在Intel的语法中基寄存器用[]括起来。
在ATT中基寄存器用()括起来
Intel语法ATT语法mov eax,[ecx]movl (%ecx),%eax
5.5.6 间接寻址方式
与Intel的语法比较ATT间接寻址方式可能更晦涩难懂一些。
Intel的指令格式是
segreg:[baseindex*scaledisp]
ATT的指令格式是
%segreg:disp(base,index,scale)格式解读
其中index/scale/disp/segreg 全部可选的完全可以简化掉。如果没有指定scale而指定了index则scale的缺省值为1.segreg段寄存器依赖于指令以及应用程序是运行在实模式还是保护模式下。在实模式下它依赖于指令在保护模式下segreg是多余的。在ATT中当立即数用在scale/disp中时不应当在其前冠以$前缀
Intel语法指令segrg:[baseindex*scaledisp]ATT语法指令%segreg:disp(base,index,scale)mov eax,[ebx10h]movl 0x10(%ebx),%eaxadd eax,[ebxecx*2h]addl (%ebx,%ecx,0x2),%eaxlea eax,[ebxecx]leal (%ebx,%ecx),%eaxsub eax,[ebxecx*4h-20h]subl -0x20(%eax,%ecx,0x4), %eax
5.5.7 总结
项目IntelATT说明操作数长度mov rax, qworld ptr[rbx]movq (%rbx),%rax一个加前缀一个加后缀寄存器命名mov rax, qworld ptr[rbx]movq (%rbx),%raxATT需要加%立即数mov ebx,0ffffhmovl **$**0xffff,%ebxATT需要加$操作数的方向mov eax,[ecx]movl (%ecx),%eax刚好相反内存单元操作数mov eax,[ecx]movl (%ecx),%eax一个使用[]一个使用()间接寻址方式segreg:[baseindex*scaledisp]%segreg:disp(base,index,scale)其实都是Intel的方式计算
5.6 函数调用详解
前面吹了这么多水终于来到我们这一篇的重点了趁着我们刚学习汇编还热乎我们来分析一下函数是怎么调用的。
5.6.1 函数参数约定
我们调用函数的时候会进行函数传参那函数是怎么知道我们传了几个参数并且每个参数的值是多少
其实这都是约定好的要命的是不同平台约定的不一样真是离了大谱增加了学习成本。
5.6.1.1 Intel方式
我们用什么方法来看这个约定的方式呢其实很简单我们直接写10个参数哈哈哈哈。 test1(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
00007FF6EDCA1958 C7 44 24 48 0A 00 00 00 mov dword ptr [rsp48h],0Ah
00007FF6EDCA1960 C7 44 24 40 09 00 00 00 mov dword ptr [rsp40h],9
00007FF6EDCA1968 C7 44 24 38 08 00 00 00 mov dword ptr [rsp38h],8
00007FF6EDCA1970 C7 44 24 30 07 00 00 00 mov dword ptr [rsp30h],7
00007FF6EDCA1978 C7 44 24 28 06 00 00 00 mov dword ptr [rsp28h],6
00007FF6EDCA1980 C7 44 24 20 05 00 00 00 mov dword ptr [rsp20h],5 // 第5个参数开始入栈了
00007FF6EDCA1988 41 B9 04 00 00 00 mov r9d,4 // 第四个参数r9
00007FF6EDCA198E 41 B8 03 00 00 00 mov r8d,3 // 第三个参数r8
00007FF6EDCA1994 BA 02 00 00 00 mov edx,2 // 第二个参数rdx
00007FF6EDCA1999 B9 01 00 00 00 mov ecx,1 // 第一个参数rcx
00007FF6EDCA199E E8 7D F9 FF FF call 00007FF6EDCA1320 是不是一看汇编就明白了这里我们就分析整数浮点不做分析后面有兴趣可以自行分析。
rcx,rdx,r8,r9用来存储整数或指针参数安装从左到右顺序xmm0,1,2,3 用来存储浮点参数其余参数会压入栈中。
5.6.1.2 ATT方式
我们接下来看看ATT方式 test1(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);40125c: 6a 0a pushq $0xa // 第10个参数40125e: 6a 09 pushq $0x9 // 第九个参数401260: 6a 08 pushq $0x8 // 第八个参数401262: 6a 07 pushq $0x7 // 第七个参数401264: 41 b9 06 00 00 00 mov $0x6,%r9d // 第六个参数40126a: 41 b8 05 00 00 00 mov $0x5,%r8d // 第五个参数401270: b9 04 00 00 00 mov $0x4,%ecx // 第四个参数401275: ba 03 00 00 00 mov $0x3,%edx // 第三个参数40127a: be 02 00 00 00 mov $0x2,%esi // 第二个参数40127f: bf 01 00 00 00 mov $0x1,%edi // 第一个参数401284: e8 c8 fe ff ff callq 401151 _Z5test1iiiiiiiiii这个也会看下汇编就懂了下面是总结
当参数在 6 个以内参数从左到右依次放入寄存器: rdi, rsi, rdx, rcx, r8, r9。多了rdi,rsi当参数大于 6 个, 大于六个的部分的依次从 “右向左” 压入栈中和32位汇编一样。
5.6.2 函数的栈帧
通过上面认识到了函数参数那我们来看看函数内部这个函数的栈是怎么使用的。
5.6.2.1 Intel方式
int test1(int i1, int i2, int i3, int i4, int i5, int i6, int i7, int i8, int i9, int i10)
{
00007FF6EDCA1810 44 89 4C 24 20 mov dword ptr [rsp20h],r9d
00007FF6EDCA1815 44 89 44 24 18 mov dword ptr [rsp18h],r8d
00007FF6EDCA181A 89 54 24 10 mov dword ptr [rsp10h],edx
00007FF6EDCA181E 89 4C 24 08 mov dword ptr [rsp8],ecx // 这是rsp把前面的4个参数入栈前面4个存储在寄存器里的需要入栈\// 我们来看看这里的rsp 000000D318CFF808 rbp 000000D318CFF860// 第一个参数 000000D318CFF808 8h 000000D318CFF810// 第二个参数 000000D318CFF808 10h 000000D318CFF818// 第三个参数 000000D318CFF808 18h 000000D318CFF820// 第10个参数 000000D318CFF808 48h 000000D318CFF850
00007FF6EDCA1822 55 push rbp //
00007FF6EDCA1823 57 push rdi
00007FF6EDCA1824 48 81 EC E8 00 00 00 sub rsp,0E8h
00007FF6EDCA182B 48 8D 6C 24 20 lea rbp,[rsp20h]
00007FF6EDCA1830 48 8D 0D 30 F8 00 00 lea rcx,[00007FF6EDCB1067h]
00007FF6EDCA1837 E8 25 FB FF FF call 00007FF6EDCA1361 return i1 i10;
00007FF6EDCA183C 8B 85 28 01 00 00 mov eax,dword ptr [rbp0000000000000128h]
00007FF6EDCA1842 8B 8D E0 00 00 00 mov ecx,dword ptr [rbp00000000000000E0h]
00007FF6EDCA1848 03 C8 add ecx,eax
00007FF6EDCA184A 8B C1 mov eax,ecx
}
00007FF6EDCA184C 48 8D A5 C8 00 00 00 lea rsp,[rbp00000000000000C8h]
00007FF6EDCA1853 5F pop rdi
00007FF6EDCA1854 5D pop rbp
00007FF6EDCA1855 C3 ret 我把Intel的函数调用的栈帧画了一个图 因为test1函数没有定义变量所以函数栈里并没有值。如果定义了变量也会跟main函数一样就是有点搞不懂每个变量离的这么远是因为怕踩到内存有知道的可以留言告诉我。
5.6.2.2 ATT方式
0000000000401151 _Z5test1iiiiiiiiii:int test1(int i1, int i2, int i3, int i4, int i5, int i6, int i7, int i8, int i9, int i10)
{401151: 55 push %rbp401152: 48 89 e5 mov %rsp,%rbp401155: 89 7d fc mov %edi,-0x4(%rbp)401158: 89 75 f8 mov %esi,-0x8(%rbp)40115b: 89 55 f4 mov %edx,-0xc(%rbp)40115e: 89 4d f0 mov %ecx,-0x10(%rbp)401161: 44 89 45 ec mov %r8d,-0x14(%rbp)401165: 44 89 4d e8 mov %r9d,-0x18(%rbp)// 这个会把其他参数都存进栈里return i1 i10;401169: 8b 55 fc mov -0x4(%rbp),%edx40116c: 8b 45 28 mov 0x28(%rbp),%eax40116f: 01 d0 add %edx,%eax
}401171: 5d pop %rbp401172: c3 retqlinux就比较节省内存因为test1没有使用临时变量所以栈大小是0。 5.6.3 函数返回值
我们都知道函数可以返回一个值那函数是怎么返回这个值的呢我们继续分析
5.6.3.1 Intel方式
char test2()
{
00007FF7DFC51870 40 55 push rbp
00007FF7DFC51872 57 push rdi
00007FF7DFC51873 48 81 EC 08 01 00 00 sub rsp,108h
00007FF7DFC5187A 48 8D 6C 24 20 lea rbp,[rsp20h]
00007FF7DFC5187F 48 8D 0D E1 F7 00 00 lea rcx,[00007FF7DFC61067h]
00007FF7DFC51886 E8 D6 FA FF FF call 00007FF7DFC51361 char s 9;
00007FF7DFC5188B C6 45 04 09 mov byte ptr [rbp4],9 return s;
00007FF7DFC5188F 0F B6 45 04 movzx eax,byte ptr [rbp4] // 返回到char型
}short test3()
{
00007FF7DFC519E0 40 55 push rbp
00007FF7DFC519E2 57 push rdi
00007FF7DFC519E3 48 81 EC 08 01 00 00 sub rsp,108h
00007FF7DFC519EA 48 8D 6C 24 20 lea rbp,[rsp20h]
00007FF7DFC519EF 48 8D 0D 71 F6 00 00 lea rcx,[00007FF7DFC61067h]
00007FF7DFC519F6 E8 66 F9 FF FF call 00007FF7DFC51361 short s 9;
00007FF7DFC519FB B8 09 00 00 00 mov eax,9
00007FF7DFC51A00 66 89 45 04 mov word ptr [rbp4],ax return s;
00007FF7DFC51A04 0F B7 45 04 movzx eax,word ptr [rbp4]
}int test4()
{
00007FF7DFC518B0 40 55 push rbp
00007FF7DFC518B2 57 push rdi
00007FF7DFC518B3 48 81 EC 08 01 00 00 sub rsp,108h
00007FF7DFC518BA 48 8D 6C 24 20 lea rbp,[rsp20h]
00007FF7DFC518BF 48 8D 0D A1 F7 00 00 lea rcx,[00007FF7DFC61067h]
00007FF7DFC518C6 E8 96 FA FF FF call 00007FF7DFC51361 int s 9;
00007FF7DFC518CB C7 45 04 09 00 00 00 mov dword ptr [rbp4],9 return s;
00007FF7DFC518D2 8B 45 04 mov eax,dword ptr [rbp4]
}long long test5()
{
00007FF7873C18E0 40 55 push rbp
00007FF7873C18E2 57 push rdi
00007FF7873C18E3 48 81 EC 08 01 00 00 sub rsp,108h
00007FF7873C18EA 48 8D 6C 24 20 lea rbp,[rsp20h]
00007FF7873C18EF 48 8D 0D 71 F7 00 00 lea rcx,[00007FF7873D1067h]
00007FF7873C18F6 E8 66 FA FF FF call 00007FF7873C1361 long long s 9;
00007FF7873C18FB 48 C7 45 08 09 00 00 00 mov qword ptr [rbp8],9 return s;
00007FF7873C1903 48 8B 45 08 mov rax,qword ptr [rbp8]
}实验证明现在是64位系统了没有数据类型比64位的长了尴尬所以测不了超过rax的时候会不会用其他寄存器了。
5.6.3.2 ATT方式
看下linux的返回值应该是一样的。
0000000000401173 _Z5test2v:char test2()
{401173: 55 push %rbp401174: 48 89 e5 mov %rsp,%rbpchar s 9;401177: c6 45 ff 09 movb $0x9,-0x1(%rbp)return s;40117b: 0f b6 45 ff movzbl -0x1(%rbp),%eax
}40117f: 5d pop %rbp401180: c3 retq 0000000000401181 _Z5test3v:short test3()
{401181: 55 push %rbp401182: 48 89 e5 mov %rsp,%rbpshort s 9;401185: 66 c7 45 fe 09 00 movw $0x9,-0x2(%rbp)return s;40118b: 0f b7 45 fe movzwl -0x2(%rbp),%eax
}40118f: 5d pop %rbp401190: c3 retq 0000000000401191 _Z5test4v:int test4()
{401191: 55 push %rbp401192: 48 89 e5 mov %rsp,%rbpint s 9;401195: c7 45 fc 09 00 00 00 movl $0x9,-0x4(%rbp)return s;40119c: 8b 45 fc mov -0x4(%rbp),%eax
}40119f: 5d pop %rbp4011a0: c3 retq 00000000004011a1 _Z5test5v:long long test5()
{4011a1: 55 push %rbp4011a2: 48 89 e5 mov %rsp,%rbplong long s 9;4011a5: 48 c7 45 f8 09 00 00 movq $0x9,-0x8(%rbp)4011ac: 00 return s;4011ad: 48 8b 45 f8 mov -0x8(%rbp),%rax
}4011b1: 5d pop %rbp4011b2: c3 retq 看来64位系统就是这么强大不需要两个寄存器了直接一个寄存器就可以返回值成功了。
5.6.4 函数退出
函数退出就比较简单了。
5.6.4.1 Intel方式
00007FF7873C1907 48 8D A5 E8 00 00 00 lea rsp,[rbp00000000000000E8h] // 处理rsp的值这样计算就是得到前面函数的rsp
00007FF7873C190E 5F pop rdi// 还记得rdi进栈不现在就弹出
00007FF7873C190F 5D pop rbp// 也继续弹出rbp
00007FF7873C1910 C3 ret // ret指令是一种汇编指令它的作用是从一个子程序中返回到调用该子程序的主程序。在执行时ret指令将从堆栈中弹出返回地址并跳转到该地址继续执行主程序。(还记得下一个执行的地址也入栈了吧)5.6.4.2 ATT方式 4011b1: 5d pop %rbp4011b2: c3 retq 从上面写的几个简单函数分析rsp的值都不会变所以函数退出只需要把rbp还原然后在ret到下一条执行的指令。
5.7 扩展ARM平台汇编分析
本来不打算介绍ARM因为我们以后都是使用Intel了ARM芯片做服务器还是比较少的但是想想如果作为比较的方式学习还是不错的。
5.7.1 RISC与CISC的区别
RISC全称Reduced Instruction Set Compute精简指令集计算机。
CISC全称Complex Instruction Set Computers复杂指令集计算机。
RISC的主要特点 1选取使用频率较高的一些简单指令以及一些很有用但不复杂的指令让复杂指令的功能由使用频率高的简单指令的组合来实现。
2指令长度固定指令格式种类少寻址方式种类少。
3只有取数/存数指令访问存储器其余指令的操作都在寄存器内完成。
4CPU中有多个通用寄存器比CICS的多
5采用流水线技术RISC一定采用流水线大部分指令在一个时钟周期内完成。采用超标量超流水线技术可使每条指令的平均时间小于一个时钟周期。
6控制器采用组合逻辑控制不用微程序控制。
7采用优化的编译程序
CICS的主要特点 1指令系统复杂庞大指令数目一般多达200~300条。
2指令长度不固定指令格式种类多寻址方式种类多。
3可以访存的指令不受限制RISC只有取数/存数指令访问存储器
4各种指令执行时间相差很大大多数指令需多个时钟周期才能完成。
5控制器大多数采用微程序控制。
6难以用优化编译生成高效的目标代码程序
RISC与CICS的比较 1.RISC比CICS更能提高计算机运算速度RISC寄存器多就可以减少访存次数指令数和寻址方式少因此指令译码较快。
2.RISC比CISC更便于设计可降低成本提高可靠性。
3.RISC能有效支持高级语言程序。
4.CICS的指令系统比较丰富有专用指令来完成特定的功能因此处理特殊任务效率高。 这个转载自https://blog.csdn.net/weixin_40491661/article/details/121351113
很明显Intel的芯片就是CICS指令现在我们来学习的ARM其实就是RISC经常使用在手机芯片上因为功耗较低。
5.7.2 ARM寄存器解读 ARM的寄存器比较多但是指令是精简的所以才是RISC的风格。
ARM的寄存器都是通用的都是R0-R15但是因为ARM有7种模式所以不同模式的寄存器用法可能不一样主要是这些模式的寄存器虽然名字相同但都是不同寄存器。
但是这里有两个寄存器比较特殊
R13栈顶指针
R15指令寄存器
5.7.3 ARM对数据的描述
ARM的发展比较晚基本没有经历过8位和16位的时代所以ARM没有什么历史负担所以对数据描述没有windows看的那么难受。
c声明Intel数据类型char字节short半字int字long双字
5.7.4 ARM汇编浅析
这个就不写了因为我现在没有交叉编译工具链等后面有机会再说吧。
参考文章
https://blog.csdn.net/weixin_39946798/article/details/113708680
https://blog.csdn.net/c2682736/article/details/122349851
https://blog.csdn.net/weixin_38633659/article/details/125221443
https://blog.csdn.net/oqqHuTu12345678/article/details/125676002
https://www.jianshu.com/p/338d2f85e954
https://blog.csdn.net/weixin_40491661/article/details/121351113