百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

C语言汇编实现分析

liebian365 2025-03-11 17:41 8 浏览 0 评论

函数调用堆栈

hello.c

#include 

int add(int a, int b) {
        a = a + 1;
        b = b + 2;
        int c = a + b;
        return c;
}

int main() {
        int i = add(1,2);
        return 0;
}

gcc -S -masm=intel hello.c -o hello_intel.s

add:
        push        ebp
        mov        ebp, esp
        sub        esp, 16
        add        DWORD PTR [ebp+8], 1
        add        DWORD PTR [ebp+12], 2
        mov        edx, DWORD PTR [ebp+8]
        mov        eax, DWORD PTR [ebp+12]
        add        eax, edx
        mov        DWORD PTR [ebp-4], eax
        mov        eax, DWORD PTR [ebp-4]
        leave
        ret
        
main:
        push        ebp
        mov        ebp, esp
        sub        esp, 16
        push        2 ;参数从右到左入栈
        push        1
        call        add
        add        esp, 8
        mov        DWORD PTR [ebp-4], eax
        mov        eax, 0
        leave
        ret

函数调用堆栈顺序

  1. 参数从右到左入栈
  2. Call(eip入栈)
  3. ebp入栈
  4. 保存用到的寄存器
  5. 业务ebp作为基址访问局部变量[ebp-n]和参数[ebp+n]
  6. 恢复用到的寄存器
  7. leave (mov esp,ebp; pop ebp)
  8. ret (eip出栈)
  9. 调用方esp+n(调用参数平栈. 8 9等价ret n)

ret; eip出栈



leave;等价于 mov esp,ebp;pop ebp



返回参数EAX



局部变量 ebp-4n访问



esp-16





ebp-4(局部变量c)



push ebp

ebp=esp


call eip 入栈


入参 ebp+8+4n访问

push 1(入参a)


push 2(入参b)





调用方esp+8, 参数a和b堆栈平衡



调用约定


参数压栈

堆栈平衡


__cdecl

从右至左入栈

调用者

c语言默认

__stdcall

从右至左入栈, this指针做为最后一个参数入栈

自身

c++


__fastcall

ECX EDX传递前2个 剩下从右至左入栈

自身

部门win32API

__thiscall

从右至左入栈, ECX传递this指针

自身


变量

全局变量

全局变量是直接生成并存储在PE文件中的,因此有固定的空间和大小

全局变量的内存地址不变. 游戏外挂中的找基址, 其实就是找全局变量.

char 如何存储和显示中文

?: if_else

c语言源代码

int calculate(int calc, int a, int b) {
        return calc <= 0 ? a + b : b - a;
}
int calculate(int calc, int a, int b) {
        if (calc <= 0) {
                return a + b;
        }
        else {
                return b - a;
        }
}

汇编解析

calculate:
        push        ebp
        mov        ebp, esp
        cmp        DWORD PTR [ebp+8], 0 ;比较
        jg        .L2
        mov        edx, DWORD PTR [ebp+12] ;?逻辑
        mov        eax, DWORD PTR [ebp+16]
        add        eax, edx
        jmp        .L4
.L2:
        mov        eax, DWORD PTR [ebp+16] ;:逻辑
        sub        eax, DWORD PTR [ebp+12]
.L4:
        pop        ebp
        ret

if_elseif_else

calculate:
        cmp        DWORD PTR [ebp+8], 0
        jne        .L2
        ;if
        jmp        .L3
.L2:
        cmp        DWORD PTR [ebp+8], 1
        jne        .L4
        ; else if
        jmp        .L3
.L4:
        ; else
.L3:
        pop        ebp

Switch

void test_switch(int n) {
    switch (n)
    {
    case 1: printf("1"); break;
    case 2: printf("2"); break;
    case 3: printf("3"); break;
    case 4: printf("4"); break;
    default:
            printf("default");
            break;
    }
}
    ;switch (n)
00414035  mov         eax,dword ptr [n]  ;参数n
00414038  mov         dword ptr [ebp-0C4h],eax  ;局部变量[ebp-0C4h] 
0041403E  mov         ecx,dword ptr [ebp-0C4h]  
00414044  sub         ecx,1              ;局部变量[ebp-0C4h]-1
00414047  mov         dword ptr [ebp-0C4h],ecx  
0041404D  cmp         dword ptr [ebp-0C4h],3  
00414054  ja          $LN7+0Fh (041409Fh)  ;如果局部变量[ebp-0C4h]>3(参数n>4)则跳转default
00414056  mov         edx,dword ptr [ebp-0C4h]  
0041405C  jmp         dword ptr [edx*4+4140C0h]  ;最终跳转算法 (n-1)*4+case表地址
     ;case 1: printf("1"); break;
00414063  push        offset string "1" (042BC30h)  
00414068  call        _printf (0411285h)  
0041406D  add         esp,4  
00414070  jmp         $LN7+1Ch (04140ACh)  ;break跳出switch
     ;case 2: printf("2"); break;
00414072  push        offset string "2" (042BC34h)  
00414077  call        _printf (0411285h)  
0041407C  add         esp,4  
0041407F  jmp         $LN7+1Ch (04140ACh)  ;break跳出switch
     ;case 3: printf("3"); break;
00414081  push        offset string "3" (042BC38h)  
00414086  call        _printf (0411285h)  
0041408B  add         esp,4  
0041408E  jmp         $LN7+1Ch (04140ACh)  ;break跳出switch
     ;case 4: printf("4"); break;
00414090  push        offset string "4" (042BC3Ch)  
00414095  call        _printf (0411285h)  
0041409A  add         esp,4  
0041409D  jmp         $LN7+1Ch (04140ACh)  ;break跳出switch
    ;default: printf("default");
0041409F  push        offset string "default" (042BC40h)  
004140A4  call        _printf (0411285h)  
004140A9  add         esp,4  
004140AC

查看Case表的数据(4140C0h)

当case项不连续时

void test_switch(int n) {
    switch (n)
    {
    case 1: printf("1"); break;
    case 3: printf("3"); break;
    case 9: printf("9"); break;
    case 2: printf("2"); break;
    case 8: printf("8"); break;
    default:
        printf("default");
        break;
    }
}

case表的数据

case 1的地址

case 2的地址

case 3的地址

default的地址

default的地址

default的地址

default的地址

case 8的地址

case 9的地址

结论: switch通过构建case表, case项的值为表的索引直接内存地址调用,因此效率高于if else

推荐用法: case项>=4且case项尽可能连续(节省case项表的大小)

while循环

int add(int a, int b) {
        int c = 0;
        do{
                c += a;
                a++;
        } while (a < b);
        return c;
}
add:
        push        ebp
        mov        ebp, esp
        sub        esp, 16
        mov        DWORD PTR [ebp-4], 0
        jmp        .L2 ;去掉变成do while循环
.L3:
        mov        eax, DWORD PTR [ebp+8] ;循环体
        add        DWORD PTR [ebp-4], eax
        add        DWORD PTR [ebp+8], 1
.L2:
        mov        eax, DWORD PTR [ebp+8] ;while判断
        cmp        eax, DWORD PTR [ebp+12]
        jl        .L3
        mov        eax, DWORD PTR [ebp-4]
        leave
        ret

for 等价于 while

int add(int a, int b) {
        int sum;
        for (sum = 0; a < b; a++) {
                sum += a;
        }
        return sum;
}
add:
        push        ebp
        mov        ebp, esp
        sub        esp, 16
        mov        DWORD PTR [ebp-4], 0 ;for第一个分号初始化
        jmp        .L2
.L3:
        mov        eax, DWORD PTR [ebp+8] ;for循环体
        add        DWORD PTR [ebp-4], eax
        add        DWORD PTR [ebp+8], 1 ;for最后一个分号计数器变化
.L2:
        mov        eax, DWORD PTR [ebp+8] ;for循环判断,和while循环完全等价
        cmp        eax, DWORD PTR [ebp+12]
        jl        .L3
        mov        eax, DWORD PTR [ebp-4]
        leave
        ret

C++多态实现分析

普通成员函数重写,不构成多态

#include
using namespace std;

class HuMan {
public:
    void show() {
        cout << "HuMan" << endl;
    }
};
class Person : HuMan {
public:
    //非虚函数重写,不构成多态
    void show() {
        cout << "Person" << endl void test_showperson person person->show();
}
int main() {
    Person p;
    test_show(&p); //调用的是HuMan::show, 输出HuMan
    return 0;
}

person.show(); 对应汇编实现,可以看出是函数的直接调用

mov         ecx,dword ptr [person]  
call        Person::show (0411B0Eh) 

虚函数重写,构成多态

#include
using namespace std;

class HuMan {
public:
    virtual void show() {
        cout << "HuMan" << endl;
    }
};
class Person : HuMan {
public:
    //虚函数重写,构成多态
    void show() {
        cout << "Person" << endl void test_showperson person person->show();
}
int main() {
    Person p;
    test_show(&p); //调用的是Person:show, 输出Person
    return 0;
}

对应汇编实现

mov         eax,dword ptr [person]  //person对象函数首地址,即虚表地址
mov         edx,dword ptr [eax]  
mov         esi,esp  
mov         ecx,dword ptr [person]  //ecx存放this指针
mov         eax,dword ptr [edx]  //虚表第一项,第二项为[edx+4]
call        eax

可以看到, p对象的第一个成员(0地址偏移)是_vfptr(虚函数表指针)

该虚函数表的第一项就是 call Person:show的跳转

多态(虚函数重写)就是通过虚函数表间接调用实现的

C++异常机制的实现方式和开销分析

参考:
https://www.baiy.cn/doc/cpp/inside_exception.htm

相关推荐

月薪 4K 到 4W 的运维工程师都经历了什么?

运维工程师在前期是一个很苦逼的工作,在这期间可能干着修电脑、掐网线、搬机器的活,显得没地位!时间也很碎片化,各种零碎的琐事围绕着你,很难体现个人价值,渐渐的对行业很迷茫,觉得没什么发展前途。这些枯燥无...

计算机专业必须掌握的脚本开发语言—shell

提起Shell脚本很多都有了解,因为无论是windows的Dom命令行还是Linux的bash都是它的表现形式,但是很多人不知道它还有一门脚本编程语言,就是ShellScript,我们提起的Shel...

Linux/Shell:排名第四的计算机关键技能

除了编程语言之外,要想找一份计算机相关的工作,还需要很多其他方面的技能。最近,来自美国求职公司Indeed的一份报告显示:在全美工作技能需求中,Linux/Shell技能仅次于SQL、Java、P...

使用Flask应用框架在Centos7.8系统上部署机器学习模型

安装centos7.8虚拟环境1、镜像链接...

shell编程

简介:Shell是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,又是一种程序设计语言。...

14天shell脚本入门学习-第二天#脚本和参数#排版修正

脚本是一种包含一系列命令的文本文件,通常用于自动化任务。Shell脚本是用Shell命令编写的脚本,可以在命令行中执行。掌握脚本的基础知识和变量的使用是编写高效脚本的关键。...

嵌入式Linux开发教程:Linux Shell

本章重点介绍Linux的常用操作和命令。在介绍命令之前,先对Linux的Shell进行了简单介绍,然后按照大多数用户的使用习惯,对各种操作和相关命令进行了分类介绍。对相关命令的介绍都力求通俗易懂,都给...

实现SHELL中的列表和字典效果

大家好,我是博哥爱运维。编写代码,很多情况下我们需要有种类型来存储数据,在python中有列表和字典,golang中有切片slice和map,那么在shell中,我们能否实现列表和字典呢,答案是肯定的...

14天shell脚本入门学习-第二天#脚本和变量

脚本是一种包含一系列命令的文本文件,通常用于自动化任务。Shell脚本是用Shell命令编写的脚本,可以在命令行中执行。掌握脚本的基础知识和变量的使用是编写高效脚本的关键。...

shell常用命令之awk用法介绍

一、awk介绍awk的强大之处,在于能生成强大的格式化报告。数据可以来自标准输入,一个或多个文件,或者其他命令的输出。他支持用户自定义函数和动态正则表达式等先进功能,是Linux/unix一个强大的文...

Linux编程Shell之入门——Shell数组拼接与合并

在Shell中,可以使用不同的方式实现数组拼接和合并。数组拼接指将两个数组中的元素合并成一个数组,而数组合并指将两个数组逐个组合成一个新数组。以下是关于Shell数组拼接和合并的详细介绍:数...

shell中如何逆序打印数组的内容,或者反转一个数组?

章节索引图首先请注意,有序的概念仅适用于索引数组,而不适用于关联数组。如果没有稀疏数组,答案会更简单,但是Bash的数组可以是稀疏的(非连续索引)。因此,我们需要引入一个额外的步骤。...

如何学好大数据开发?---shell基本语法

昨天我们初步了解到了shell的一些基本知识,比如shell的分类,常用的shell类型。今天就带来大数据开发之shell基本语法,掌握好基础才是最重要的,那接下来就开始学习shell的基本语法。一、...

Linux编程Shell之入门——Shell关联数组

关联数组是Shell中一种特殊的数组类型,它使用字符串作为下标。在关联数组中,每个元素都被标识为一个唯一的字符串键值,也称为关联数组的索引。在Shell中,可以使用declare或typeset命令...

从编译器视角看数组和指针

虽然有单独的文章描述数组和指针,但二者的关系实在值得再写一篇文章。...

取消回复欢迎 发表评论: