【汇编逆向系列】六、函数调用包含多个参数之多个整型-参数压栈顺序,rcx,rdx,r8,r9寄存器

发布于:2025-06-06 ⋅ 阅读:(17) ⋅ 点赞:(0)

从本章节开始,进入到函数有多个参数的情况,前面几个章节中介绍了整型和浮点型使用了不同的寄存器在进行函数传参,ECX是整型的第一个参数的寄存器,那么多个参数的情况下函数如何传参,下面展开介绍参数为整型时候的几种情况:

1. 两个参数

1.1 汇编代码

1.1.1 debug编译

two_int_params:
  0000000000000170: 89 54 24 10        mov         dword ptr [rsp+10h],edx
  0000000000000174: 89 4C 24 08        mov         dword ptr [rsp+8],ecx
  0000000000000178: 57                 push        rdi
  0000000000000179: 8B 44 24 10        mov         eax,dword ptr [rsp+10h]
  000000000000017D: 0F AF 44 24 18     imul        eax,dword ptr [rsp+18h]
  0000000000000182: 8B 4C 24 18        mov         ecx,dword ptr [rsp+18h]
  0000000000000186: 8B 54 24 10        mov         edx,dword ptr [rsp+10h]
  000000000000018A: 03 D1              add         edx,ecx
  000000000000018C: 8B CA              mov         ecx,edx
  000000000000018E: 03 C1              add         eax,ecx
  0000000000000190: 5F                 pop         rdi
  0000000000000191: C3                 ret
  0000000000000192: CC                 int         3

1.2.1 release编译

two_int_params:
  0000000000000000: 8D 42 01           lea         eax,[rdx+1]
  0000000000000003: 0F AF C1           imul        eax,ecx
  0000000000000006: 03 C2              add         eax,edx
  0000000000000008: C3                 ret

1.2. 汇编分析

1.2.1 函数参数

Microsoft x64 调用约定参数传递的整数、指针参数,前四个参数依次使用:

  • RCX(第1个)、RDX(第2个)、R8(第3个)、R9(第4个)
  • 超过 4 个的参数从右向左压入栈(如第5个参数在 [RSP+0x20]

在本例子中rcx和rdx分别保存第一个和第二个参数

1.2.2 imul指令

有符号乘法指令,有2中操作数格式

1.2.2.1 单操作数格式

IMUL reg/mem:被乘数隐含在累加器(AL/AX/EAX/RAX),结果存于扩展寄存器对

语法格式为:

                                                          IMUL src

操作流程:

                      (AL/AX/EAX/RAX) × src → AX/DX:AX/EDX:EAX/RDX:RAX

将(AL/AX/EAX/RAX)与src相乘存放到 (AL/AX/EAX/RAX)寄存器

标志位影响:若高半部分非符号扩展,则CF=OF=1

RAX寄存器详见上一节对RAX寄存器的介绍, OF为有符号的溢出的标值寄存器

示例:

MOV AL, 3      ; AL=03H(+3)
MOV BL, 6      ; BL=06H(+6)
IMUL BL        ; AX=0012H(+18)
结果:AX=0012H → 高半部分AH=00H是AL=12H(正数)的符号扩展(补0)。
标志位:CF=OF=0(无需关注高半部)
MOV AL, 48     ; AL=30H(+48)
MOV BL, 4      ; BL=04H(+4)
IMUL BL        ; AX=00C0H(192)
结果:AX=00C0H → AH=00H ​​不是​​ AL=C0H(C0H作为有符号数为-64)的符号扩展(应为FFH)。
标志位:CF=OF=1(高半部AH=00H含有效数据,实际结果192超8位有符号范围-128~127
MOV AL, -4     ; AL=FCH(-4)
MOV BL, 4      ; BL=04H(+4)
IMUL BL        ; AX=FFF0H(-16)
结果:AX=FFF0H → 高半部分AH=FFH是AL=F0H(负数)的符号扩展(补1)。
标志位:CF=OF=0

 总结OF和CF的用法:

 💡 ​​关键设计​​:
单操作数乘法结果需存于双倍位宽的寄存器(如16位乘→32位结果)。当CF=OF=1时,表明​​低半部分无法完整表示结果​​,必须联合高半部分使用

1.2.2.2 双操作数格式

双操作数的语法格式为:

                                                     IMUL dest, src

目标寄存器dest与源操作数src相乘,结果直接存入dest(按dest大小截断)

                                                    dest × src → dest

若有效位被截断(如乘积超出dest位数),则CF=OF=1

示例:

mov  ax, -16     ; AX = FFF0h (-16)
mov  bx, 2       ; BX = 0002h
imul bx, ax      ; BX = AX × BX = FFE0h (-32) → 未溢出,OF=0[2,6](@ref)
mov  ax, 32000   ; AX = 7D00h (32000)
imul ax, 2       ; AX = FA00h (-1536) → 实际应为64000(超16位范围),截断导致OF=1[6](@ref)
 1.2.2.3 三操作数格式

语法格式为:

                                            IMUL dest, src1, imm

源操作数src1与立即数imm相乘,结果存入目标寄存器dest:

                                                 src1 × imm → dest

同双操作数,截断时CF=OF=1

示例:

imul ebx, eax, 4     ; EBX = EAX × 4(例:EAX=-16 → EBX=FFFFFFC0h (-64))[4,6](@ref)
imul bx, word1, -16   ; BX = [word1] × (-16)(若word1=4 → BX=FFC0h (-64))[6](@ref)
imul ebx, dword1, -2000000000  ; 乘积超32位 → 截断,OF=1[6](@ref)

3. 汇编转化

1.3.1 debug编译

0000000000000170: mov dword ptr [rsp+10h], edx  ; 保存第二个参数到栈 [rsp+10h]
0000000000000174: mov dword ptr [rsp+8], ecx    ; 保存第一个参数到栈 [rsp+8]
0000000000000178: push rdi                      ; 保存 rdi(被调用者保存寄存器)
0000000000000179: mov eax, dword ptr [rsp+10h]  ; 加载第一个参数到 eax
000000000000017D: imul eax, dword ptr [rsp+18h] ; eax = eax * 第二个参数(从栈加载)
0000000000000182: mov ecx, dword ptr [rsp+18h]  ; 加载第二个参数到 ecx
0000000000000186: mov edx, dword ptr [rsp+10h]  ; 加载第一个参数到 edx
000000000000018A: add edx, ecx                  ; edx = edx + ecx(两参数相加)
000000000000018C: mov ecx, edx                  ; ecx = 相加结果
000000000000018E: add eax, ecx                  ; eax = 乘积结果 + 相加结果
0000000000000190: pop rdi                       ; 恢复 rdi
0000000000000191: ret                           ; 返回(结果在 eax)
0000000000000192: int 3                         ; 断点指令(调试用)

1.3.2 release编译

0000000000000000: lea eax, [rdx+1]      ; eax = rdx + 1(第二个参数加1)
0000000000000003: imul eax, ecx         ; eax = eax * ecx(乘以第一个参数)
0000000000000006: add eax, edx          ; eax = eax + edx(再加第二个参数)
0000000000000008: ret                   ; 返回

 相较于debug编译优化了中间所有的单步操作(因为debug需要能够断电调试)

1.3.3 c语言转化

int two_int_params(int param1, int param2) {
    return (param2 + 1) * param1 + param2;
}

2.  大量整型参数

2.1 汇编代码

2.1.1 debug编译

many_int_params:
  0000000000000960: 44 89 4C 24 20     mov         dword ptr [rsp+20h],r9d
  0000000000000965: 44 89 44 24 18     mov         dword ptr [rsp+18h],r8d
  000000000000096A: 89 54 24 10        mov         dword ptr [rsp+10h],edx
  000000000000096E: 89 4C 24 08        mov         dword ptr [rsp+8],ecx
  0000000000000972: 57                 push        rdi
  0000000000000973: 8B 44 24 18        mov         eax,dword ptr [rsp+18h]
  0000000000000977: 8B 4C 24 10        mov         ecx,dword ptr [rsp+10h]
  000000000000097B: 03 C8              add         ecx,eax
  000000000000097D: 8B C1              mov         eax,ecx
  000000000000097F: 03 44 24 20        add         eax,dword ptr [rsp+20h]
  0000000000000983: 03 44 24 28        add         eax,dword ptr [rsp+28h]
  0000000000000987: 03 44 24 30        add         eax,dword ptr [rsp+30h]
  000000000000098B: 03 44 24 38        add         eax,dword ptr [rsp+38h]
  000000000000098F: 03 44 24 40        add         eax,dword ptr [rsp+40h]
  0000000000000993: 03 44 24 48        add         eax,dword ptr [rsp+48h]
  0000000000000997: 03 44 24 50        add         eax,dword ptr [rsp+50h]
  000000000000099B: 03 44 24 58        add         eax,dword ptr [rsp+58h]
  000000000000099F: 5F                 pop         rdi
  00000000000009A0: C3                 ret
  00000000000009A1: CC                 int         3
  00000000000009A2: CC                 int         3
  00000000000009A3: CC                 int         3

2.1.2 release编译

many_int_params:
  0000000000000000: 8D 04 11           lea         eax,[rcx+rdx]
  0000000000000003: 41 03 C0           add         eax,r8d
  0000000000000006: 41 03 C1           add         eax,r9d
  0000000000000009: 03 44 24 28        add         eax,dword ptr [rsp+28h]
  000000000000000D: 03 44 24 30        add         eax,dword ptr [rsp+30h]
  0000000000000011: 03 44 24 38        add         eax,dword ptr [rsp+38h]
  0000000000000015: 03 44 24 40        add         eax,dword ptr [rsp+40h]
  0000000000000019: 03 44 24 48        add         eax,dword ptr [rsp+48h]
  000000000000001D: 03 44 24 50        add         eax,dword ptr [rsp+50h]
  0000000000000021: C3                 ret

2.2 汇编分析

为了更加方便理解,下面将使用这段汇编的原始C代码进行反向逆推,直接给出C源码:

// 大量参数函数(超过寄存器数量,测试栈传递)
int many_int_params(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, int p9, int p10) {
    // 这个函数会强制使用栈传递参数(在x64上,前4个整数参数通过寄存器传递)
    return p1 + p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9 + p10;
}

下面会以这个C函数举例

2.2.1 大量参数

接1.2.1章节当参数参数超过4个的时候,如何用栈空间传递参数

2.2.1.1 函数调用规定

根据 ​​x64 快速调用约定​​(MSVC 默认规则)

  • ​寄存器传递​​:前 4 个整型参数通过寄存器传递:
    • p1 → RCX
    • p2 → RDX
    • p3 → R8
    • p4 → R9
  • ​栈传递​​:第 5–10 个参数通过栈传递,​​从右向左压栈​​(即 p10 先入栈,p5 最后入栈)。
  • ​影子空间(Shadow Space)​​:调用者需预留 ​​32 字节(0x20)​​ 栈空间,供被调函数保存寄存器参数(即使未使用)
  • 栈对齐​​:调用时 RSP 必须 ​​16 字节对齐​​(地址能被 16 整除)
2.2.1.2 调用者(caller)的栈操作

假设在 main 函数中调用 many_int_params(1,2,...,10),调用者需完成以下步骤:

  • 预留栈顶空间
sub rsp, 58h         ; RSP -= 0x58 (88 字节)

 ​​空间构成​​:

        影子空间:32 字节(0x20

        额外参数空间:6 参数 × 8 字节 = 48 字节(0x30

        总计:0x20 + 0x30 = 0x50 → 额外预留 8 字节满足 16 字节对齐(0x58 对齐到 0x60),由于call操作会额外的将rsp-=8, 为了满足在call指令之后栈对齐,这里要再预留8字节空间

  • 压入第 5–10 个参数(从右向左)

mov qword ptr [rsp+50h], 10   ; p10 → [RSP+0x50]
mov qword ptr [rsp+48h], 9   ; p9 → [RSP+0x48]
...
mov qword ptr [rsp+20h], 5    ; p5  → [RSP+0x20]

 栈偏移逻辑​​:

         参数 p5 位于影子空间上方(RSP+0x20

         后续参数地址递增 8 字节(p6RSP+0x28, ..., p12RSP+0x50

  • 设置寄存器参数​

mov rcx, 1    ; p1 → RCX
mov rdx, 2    ; p2 → RDX
mov r8, 3     ; p3 → R8
mov r9, 4     ; p4 → R9
  •  执行call指令
call many_int_params   ; 1. 返回地址压栈(RSP -= 8)
                       ; 2. 跳转到函数入口

 SP 变化​​:CALL 隐含 push RIP 操作 → RSP -= 8

2.2.1.3 被调用函数(callee)的栈帧变化
; 1. 保存寄存器参数到栈(覆盖影子空间)
mov [rsp+20h], r9d   ; p4 → [rsp+20h]
mov [rsp+18h], r8d   ; p3 → [rsp+18h]
mov [rsp+10h], edx    ; p2 → [rsp+10h]
mov [rsp+8], ecx      ; p1 → [rsp+8]

; 2. 保存非易失寄存器 (RSP -= 8)
push rdi             ; RSP 下移 8 字节

; 3. 计算求和(从栈加载参数)
mov eax, [rsp+18h]   ; 加载 p1,由于push rdi会使rsp下移8字节,p1地址从[rsp+8]变成了[rsp+10h]
mov ecx, [rsp+10h]   ; 加载 p2
add ecx, eax         ; p2 + p1
...
add eax, [rsp+58h]   ; 累加 p10

; 4. 恢复栈帧
pop rdi              ; RSP += 8 (恢复 RDI)
ret                  ; RSP += 8 (弹出返回地址)

这里要注意的是:

由于call函数会使rsp-=8, 原来影子空间的地址会从[rsp]-[rsp+20h]变成[rsp+8]-[rsp+28h],原来p5从[rsp+20]的位置变成[rsp+28],后面参数依次下推。push rdi之后,再次rsp-=8, 原来的参数的地址会再度变化p1-p10从[rsp+8]-[rsp+58h]变化到[rsp+10h]-[rsp+60h]


网站公告

今日签到

点亮在社区的每一天
去签到