手写操作系统 --汇编执行流(二)

发布时间:2023年12月29日

思考

  • 如何用汇编编写带参数的执行流?
  • C语言的传参,汇编层面是如何实现的?
  • 函数参数过多,汇编层面是如何实现的?

如何用汇编编写带参数的执行流

  • 考虑通过寄存器传参
  • 考虑通过栈传参
  • 考虑通过寄存器+栈传参
  • 基本结构实现还是精简结构实现?

汇编实现一个带参函数

用汇编写一个带参数的函数需要注意:

  1. 汇编层面是体现不出来有没有带参数
  2. 如果带参数,记得写函数原型
; 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模式下

  1. 对于x86架构(32位)GCC默认使用__cdecl作为其调用约定。cdecl(C declaration)是最常见的调用约定,特别是在Unix、Linux和其他POSIX系统上。
  2. 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寄存器 + 栈的方式传参,用了两个寄存器:ecxedx
    • 自右向左,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
      • 前八个浮点参数传递给XMM0XMM7
      • 超过这些限制的参数通过堆栈传递。
在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
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。