手写操作系统 --汇编执行流(二)
2023-12-29 12:06:48
思考
- 如何用汇编编写带参数的执行流?
- C语言的传参,汇编层面是如何实现的?
- 函数参数过多,汇编层面是如何实现的?
如何用汇编编写带参数的执行流
- 考虑通过寄存器传参
- 考虑通过栈传参
- 考虑通过寄存器+栈传参
- 基本结构实现还是精简结构实现?
汇编实现一个带参函数
用汇编写一个带参数的函数需要注意:
- 汇编层面是体现不出来有没有带参数
- 如果带参数,记得写函数原型
; void add(int a, int b);
------------------------------ ;汇编函数框架
global add
add:
push ebp
mov ebp, esp
leave
ret
-------------------------------
以具体得函数举例子
#include <iostream>
int Print(int a, int b)
{
int c = 10;
return a + b + c;
}
int main()
{
int a = 10;
Print(1, 2);
return 0;
}
;对应cpp汇编
16: Print(1, 2);
001C258F push 2
001C2591 push 1 ;从右向左
001C2593 call 001C1384
001C2598 add esp,8 ;外平栈 堆栈平衡
6: int Print(int a, int b)
7: {
001C16F0 push ebp
001C16F1 mov ebp,esp ;创建栈帧
001C16F3 sub esp,0CCh
-------------------------------------------
001C16F9 push ebx
001C16FA push esi ;保存上下文
001C16FB push edi
-------------------------------------------
001C16FC lea edi,[ebp+FFFFFF34h]
001C1702 mov ecx,33h
001C1707 mov eax,0CCCCCCCCh ;初始化栈
001C170C rep stos dword ptr es:[edi]
-------------------------------------------
8: int c = 10;
001C1718 mov dword ptr [ebp-8],0Ah
9:
10: return a + b + c; ;业务逻辑
001C171F mov eax,dword ptr [ebp+8]
001C1722 add eax,dword ptr [ebp+0Ch]
001C1725 add eax,dword ptr [ebp-8]
-------------------------------------------
11: }
001C1728 pop edi
001C1729 pop esi ;恢复上下文
001C172A pop ebx
-------------------------------------------
001C1738 mov esp,ebp
001C173A pop ebp ;释放栈帧
-------------------------------------------
001C173B ret
根据对应代码回答几个问题:
- 当采用
MVC
编译时,虽然没有写调用约定,但是默认的调用约定是__cdecl
- 用时,参数的压栈顺序是从右向左,还是从左向右?
- 依靠栈传参
- 传参的顺序:从右向左(所有的调用约定)
- 传递的参数是在哪个栈里?
_tmain
(调用者)还是print
(被调用者)?- 调用者栈中
_tmain
- 调用者栈中
print函数
中怎么取到参数?- 借助
ebp
寄存器 - 第一个参数的计算公式要记住:
ebp+8
ebp + 0xc
32位:ebp + 4 * 2
(非常重要!!!)
64位:rbp + 8 * 2
- 借助
- 因为传参破坏了栈平衡,由谁来平栈
- 内平栈:被调用函数自己来平衡栈,如
ret 4 * N
,其中N
为参数个数 - 外平栈:调用者来平衡栈
- 内平栈:被调用函数自己来平衡栈,如
上文描述的汇编执行流图:
关于传参
- 用者传参,汇编层面是如何实现的?
- 被调用者如何拿到参数,汇编层面是如何实现的
- 传参与局部变量,汇编层面的实现,是否是一样的?
函数调用约定
__cdecl
__stdcall
__fastcall
x86模式下
- 对于
x86架构(32位)
,GCC
默认使用__cdecl
作为其调用约定。cdecl(C declaration)
是最常见的调用约定,特别是在Unix、Linux
和其他POSIX
系统上。- 于
32位Windows(x86架构)
,许多Windows API
函数使用stdcall
作为其默认调用约定。
__cdecl
- 传参方式及传参顺序
- 只借助栈
- 自右向左
- 平栈的方式
- 外平栈
__stdcall
- 传参方式及传参顺序
- 只借助栈
- 自右向左
- 平栈的方式
- 内平栈
// 采用__stdcall调用约定
#include <iostream>
int __stdcall Print(int a, int b)
{
int c = 10;
return a + b + c;
}
int main()
{
int a = 10;
Print(1, 2);
return 0;
}
; 对比__cdecle调用约定的主要代码
16: Print(1, 2);
0005258F 6A 02 push 2
00052591 6A 01 push 1
00052593 E8 F1 ED FF FF call 00051389
;没有了 add esp,8的外平栈
6: int __stdcall Print(int a, int b)
{....
0005173B C2 08 00 ret 8 ;内平栈
__fastcall
- 传参方式及传参顺序
- 会借助寄存器传参 总计6 + 8 = 14个寄存器
- 当
参数 <= 2
时纯寄存器
传参- 当
参数 > 2
时寄存器 + 栈
的方式传参,用了两个寄存器:ecx
、edx
- 自右向左,
edx
是第二个
参数,ecx
是第一个
参数- 平栈的方式
- 当
纯寄存器
传参时不需要平栈
- 当
寄存器 + 栈
传参时采用内平栈
只有两个参数使用纯寄存器传参:
// 采用__fastcall调用约定
#include <iostream>
int __fastcall Print(int a, int b)
{
int c = 10;
return a + b + c;
}
int main()
{
int a = 10;
Print(1, 2);
return 0;
}
; 对比__cdecle调用约定的主要代码
16: Print(1, 2);
0101258F BA 02 00 00 00 mov edx,2 ;使用寄存器传参
01012594 B9 01 00 00 00 mov ecx,1
01012599 E8 F0 ED FF FF call 0101138E
;没有了 add esp,8的外平栈
int __fastcall Print(int a, int b)
7: { .....
01013D13 C3 ret ;不需要平栈
超过两个参数使用寄存器 + 栈的方式传参:
- 用了两个寄存器:
ecx、edx
- 自右向左
edx
是第二个参数,ecx
第一个参数- 前两个参数用寄存器传参,后两个参数用栈传参
- 内平栈
// 采用__fastcall调用约定
#include <iostream>
int __fastcall Print(int a, int b)
{
int c = 10;
return a + b + c;
}
int main()
{
int a = 10;
Print(1, 2);
return 0;
}
; 对比只有两个参数时使用__fastcall调用约定的主要代码
16: Print(1, 2, 3, 4);
0055258F 6A 04 push 4
00552591 6A 03 push 3 ;前两个参数用寄存器传参,后两个参数用栈传参
00552593 BA 02 00 00 00 mov edx,2
00552598 B9 01 00 00 00 mov ecx,1
0055259D E8 F1 ED FF FF call 00551393
int __fastcall Print(int a, int b)
7: { .....
00553D13 C2 08 00 ret 8 ;内平栈
x64模式下
对于
x64架构
,情况有所不同。x64
平台基本上统一了函数调用约定
,不同于x86有多种调用约定。在Unix-like系统(如Linux)
上,x64
使用System V ABI
,而在Windows
上使用x64 calling convention
。这意味着在x64
上,不论是GCC
还是其他编译器,都使用相同的调用约定
。
__fastcall
- 传参方式及传参顺序
- 会借助寄存器传参
- 当
参数 <= 6
时纯寄存器
传参- 当
参数 > 6
时寄存器 + 栈
的方式传参- 前六个整数或指针参数传递给
RDI
,RSI
,RDX
,RCX
,R8
,R9
。- 前八个浮点参数传递给
XMM0
到XMM7
。- 超过这些限制的参数通过堆栈传递。
在x64架构下,与x86相比,函数调用约定已经得到了简化。在Windows和Unix-based系统(如Linux)下的x64函数调用约定有所不同。以下是两者的简要概述:
### Windows x64 Calling Convention (__fastcall):
1. **寄存器传参**:
- 前四个整数或指针参数传递给`RCX`, `RDX`, `R8`, `R9`。
- 前四个浮点参数传递给`XMM0`, `XMM1`, `XMM2`, `XMM3`。
- 如果有更多的参数,它们将通过堆栈传递。
2. **调用者保存寄存器**:
- 调用者负责保存`RAX`, `RCX`, `RDX`, `R8`, `R9`, `R10`, `R11`以及`XMM0`到`XMM5`。
3. **被调用者保存寄存器**:
- 被调用函数(如果它们被修改)负责保存`RBX`, `RSI`, `RDI`, `RSP`, `RBP`, `R12`到`R15`,以及`XMM6`到`XMM15`。
4. **堆栈对齐**:
- 必须保证堆栈(RSP)在函数调用前是16字节对齐的。
### System V ABI (Unix-based, e.g., Linux) x64 Calling Convention:
1. **寄存器传参**:
- 前六个整数或指针参数传递给`RDI`, `RSI`, `RDX`, `RCX`, `R8`, `R9`。
- 前八个浮点参数传递给`XMM0`到`XMM7`。
- 超过这些限制的参数通过堆栈传递。
2. **调用者保存寄存器**:
- 调用者负责保存`RAX`, `RCX`, `RDX`, `RSI`, `RDI`, `R8`, `R9`, `R10`, `R11`以及`XMM0`到`XMM15`。
3. **被调用者保存寄存器**:
- 被调用函数(如果它们被修改)负责保存`RBX`, `RSP`, `RBP`, `R12`到`R15`。
4. **堆栈对齐**:
- 与Windows一样,堆栈(RSP)在函数调用前也必须是16字节对齐的。
以上仅仅是函数调用约定的基本点。
文章来源:https://blog.csdn.net/weixin_53492721/article/details/133361839
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!