01 栈上变量

C 语言中,栈上变量的位置是这样的:

C语言栈上变量

Intel 的 x86 系列 CPU——小端存储(低位放到低地址

比如 0x000000C8 存储在计算机中:
低位 --> 高位
C8 00 00 00

02 static

先看 C 语言代码:

int func(int i)
{
    static int a = i;
    static int b = i+1;
    // other code
}

下面是汇编代码:

    xor eax, eax
    mov al, byte_4255D8 ; 这个存储 static 是否被赋值
    and eax, 1 ; 只保留最低位
    test eax, eax
    jnz short loc_40105E

    ; 如果没有初始化过这个 static
    mov cl, byte_4255D8
    or cl, 1 ; 将最低位赋值为 1
    mov byte_4255D8, cl
    mov edx, [ebp+arg_0]
    mov dword_4255DC, edx ; 初始化static为函数第一个参数

loc_40105E:
    xor eax, eax
    mov al, byte_4255D8 ; 这个存储 static 是否被赋值
    and eax, 2 ; 只保留倒数第二位
    test eax, eax
    jnz short loc_401087

    ; 如果没有初始化过这个 static
    mov cl, byte_4255D8
    or cl, 2 ; 将倒数第二位赋值为 1
    mov byte_4255D8, cl
    mov edx, [ebp+arg_0]
    add edx, 1
    mov dword_4255E0, edx ; 初始化static为函数第一个参数+1

03 switch

先看 C 语言:

    switch(a)
    {
    case 1:
        // xxx
        break;
    case 2:
        // xxx
        break;
    case 3:
        // xxx
        break;
    case 4:
        // xxx
        break;
    default:
        // xxx
    }

再看汇编:

    mov edx, [ebp+var_4]
    jmp ds:off_4010D2[edx*4]

.text
off_4010D2:
    dd offset loc_401047
    dd offset loc_4010xx
    dd offset loc_4010xx
    dd offset loc_4010xx
    dd offset loc_4010xx

注意:

  • 当 case 数量减少时,会变成和 if-elif-else 类似

    • 但是最后会有一个单入单出的 jmp 指令

      case 数量减少

第二种情况,case不是单增且线性的。先看 C 语言:

    switch(a)
    {
    case 1:
        // xxx
        break;
    case 2:
        // xxx
        break;
    case 3:
        // xxx
        break;
    case 4:
        // xxx
        break;
    case 255:
        // xxx
        break;
    default:
        // xxx
    }

再看汇编:

    mov edx, [ebp+var_4]
    xor edx, edx
    mov dl, ds:byte_4010F7[eax]
    jmp ds:off_4010D2[edx*4]

byte_4010F7:
    db 0, 1, 2, 3, 5, 5,..., 5, 4

.text
off_4010D2:
    dd offset loc_401047
    dd offset loc_4010xx
    dd offset loc_4010xx
    dd offset loc_4010xx
    dd offset loc_4010xx
    dd offset loc_4010xx

04 循环

while

    int i=100;
    while(i--)
    {
        // xxx
    }
loc_40102F:
    mov eax, [ebp+var_4] ; 因为我们是 i--,先判断再减
    mov ecx, [ebp+var_4]
    sub ecx, 1
    mov [ebp+var_4], ecx
    test eax, eax
    jz short loc_401052 ; 跳出循环

    ; 循环内部
    jmp short loc_40102F ; 跳回循环开头

loc_401052:
    ; 后续代码

for

    int i=100;
    for(;i;i--)
    {
        // xxx
    }
loc_401031: ; 这里不是循环入口点
    mov eax, [ebp+var_4]
    sub eax, 1
    mov [ebp+var_4], eax ; i--
    
loc_40103A: ; 这里才是循环入口点
    cmp [ebp+var_4], 0
    jz short loc_401053 ; 跳出循环

    ; 循环内部
    jmp short loc_401031 ; 跳回开头


loc_401053:
    ; 后续代码

do-while

    int i=100;
    do
    {
        i--;
        // xxx
    } while(i);
loc_40102F: ; 循环入口
    mov eax, [ebp+var_4]
    sub eax, 1
    mov [ebp+var_4], eax ; i--
    
    ; 循环内部
    
    cmp [ebp+var_4], 0
    jnz short loc_40102F ; 跳回开头

    ; 后续代码

05 函数

    mov eax, [ebp+var_4]
    push eax
    call foo
    add esp, 4 ; 手动清栈,去除掉多的esp

foo:
    ; 计算栈顶栈底
    push ebp
    mov ebp, esp
    sub esp, 44h
    ; 开始保存母函数环境
    push ebx
    push esi
    push edi
    ; 函数内部逻辑
    ; ...
    ; 恢复母函数环境
    pop edi
    pop esi 
    pop ebx
    ; 计算栈顶栈底
    add esp, 44h
    cmp ebp, esp
    call __chkesp
    mov esp, ebp
    pop ebp
    retn

函数调用流程:

  • 使用 push 将参数压入栈

  • 在 call 的时候,将返回地址(call 下一行的地址)压入栈,然后进入函数内部

函数执行流程:

  • 使用 push 将母函数栈底压入栈

  • 使用 mov 将子函数的栈底设为母函数的栈顶

  • 通过 sub 计算子函数栈顶

  • 使用 push 保存母函数的寄存器

  • <程序执行>

  • 使用 pop 恢复母函数的寄存器

  • 通过 add 恢复母函数栈顶

  • 使用 cmp 比较堆栈是否平衡

  • 通过 mov 恢复母函数栈顶

  • 通过 pop 恢复母函数栈底

  • 通过 ret 返回母函数返回地址

函数调用约定——Cdecl

参数从右向左依次压入栈

函数调用约定——Stdcall

函数调用约定——Fastcall