从汇编来角度剖析C语言函数调用过程

news2024/11/20 9:37:24

目录

1.引言

2.寄存器

3.栈帧

4.函数调用前调用者的动作

5.被调用者在函数调用后的动作

6.被调用者返回前的动作

7.调用者在返回后的动作

8.总结


1.引言

        当一个c函数被调用时,一个栈帧(stack frame)是如何被建立,又如何被消除的。这些细节跟操作系统平台及编译器的实现有关,下面的描述是针对运行在Intel奔腾芯片上Linux的gcc编译器而言。c语言的标准并没有描述实现的方式,所以,不同的编译器,处理器,操作系统都可能有自己的建立栈帧的方式。

2.寄存器

寄存器是集成到CPU内部的用来存放数据的一些小型存储区域,可以暂时存放参与运算的数据和运算结果。分为标志寄存器FR,指令指针寄存器IP,段寄存器,指针和变址寄存器,通用寄存器组等……

通用常见寄存器主要有以下几个:

寄存器名称功能
eax累加寄存器,相对于其他寄存器,在进行运算方面比较常用。
ebx基地址寄存器,通常作为内存偏移指针使用(相对于EAX、 ECX、EDX)
ecx计数寄存器,通常用于特定指令的计数,可用于循环操作。比如:重复的字符存储操作,或者数字统计
edi通常在内存操作指令中作为“目的地址指针”使用。当然, EDI可以被装入任意的数值, 但通常没有人把它当作通用寄存器来用
esi通常在内存操作指令中作为“源地址指针”使用。当然, ESI也可以被装入任意的数值, 但通常没有人把它当作通用寄存器来用。
esp为扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针。它会随着我们栈空间的大小变化,从而改变其所指地址的位置,以适应栈帧空间的大小变化
ebp为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。作为一个完整函数所对应的栈帧空间的底线

3.栈帧

          上图是一个典型的栈帧,图中,栈顶在上,地址空间往下增长。
          这是如下一个函数调用时的栈的内容:

    int foo(int arg1, int arg2, int arg3);

        并且,foo有两个局部的int变量(4个字节)。在这个简化的场景中,main调用foo,而程序的控制仍在foo中。这里,main是调用者(caller),foo是被调用者(callee)。
        ESP被foo使用来指示栈顶。EBP相当于一个“基准指针”。从main传递到foo的参数以及foo本身的局部变量都可以通过这个基准指针为参考,加上偏移量找到。
        由于被调用者允许使用EAX,ECX和EDX寄存器,所以如果调用者希望保存这些寄存器的值,就必须在调用子函数之前显式地把他们保存在栈中。另一方面,如果除了上面提到的几个寄存器,被调用者还想使用别的寄存器,比如EBX,ESI和EDI,那么,被调用者就必须在栈中保存这些被额外使用的寄存器,并在调用返回前回复他们。也就是说,如果被调用者只使用约定的EAX,ECX和EDX寄存器,他们由调用者负责保存并回复,但如果被调用这还额外使用了别的寄存器,则必须有他们自己保存并回复这些寄存器的值。
        传递给foo的参数被压到栈中,最后一个参数先进栈,所以第一个参数是位于栈顶的。foo中声明的局部变量以及函数执行过程中需要用到的一些临时变量也都存在栈中。
        小于等于4个字节的返回值会被保存到EAX,如果大于4字节,小于8字节,那么EDX也会被用来保存返回值。如果返回值占用的空间还要大,那么调用者会向被调用者传递一个额外的参数,这个额外的参数指向将要保存返回值的地址。用C语言来说,就是函数调用:

    x = foo(a, b, c);

        被转化为:

    foo(&x, a, b, c);

        注意,这仅仅在返回值占用大于8个字节时才发生。有的编译器不用EDX保存返回值,所以当返回值大于4个字节时,就用这种转换。
当然,并不是所有函数调用都直接赋值给一个变量,还可能是直接参与到某个表达式的计算中,如:

    m = foo(a, b, c) + foo(d, e, f);

        有或者作为另外的函数的参数, 如:

    fooo(foo(a, b, c), 3);

        这些情况下,foo的返回值会被保存在一个临时变量中参加后续的运算,所以,foo(a, b, c)还是可以被转化成foo(&tmp, a, b, c)

        让我们一步步地看一下在c函数调用过程中,一个栈帧是如何建立及消除的。

4.函数调用前调用者的动作

        在我们的例子中,调用者是main,它准备调用函数foo。在函数调用前,main正在用ESP和EBP寄存器指示它自己的栈帧。

        首先,main把EAX,ECX和EDX压栈。这是一个可选的步骤,只在这三个寄存器内容需要保留的时候执行此步骤。
        接着,main把传递给foo的参数一一进栈,最后的参数最先进栈。例如,我们的函数调用是:

    a = foo(12, 15, 18);

        相应的汇编语言指令是:

    push dword 18
    push dword 15
    push dword 12

        最后,main用call指令调用子函数:

    call foo

        当call指令执行的时候,EIP指令指针寄存器的内容被压入栈中。因为EIP寄存器是指向main中的下一条指令,所以现在返回地址就在栈顶了。在call指令执行完之后,下一个执行周期将从名为foo的标记处开始。
        下图展示了call指令完成后栈的内容。下图及后续图中的粗线指示了函数调用前栈顶的位置。我们将会看到,当整个函数调用过程结束后,栈顶又回到了这个位置。

5.被调用者在函数调用后的动作

        当函数foo,也就是被调用者取得程序的控制权,它必须做3件事:建立它自己的栈帧,为局部变量分配空间,最后,如果需要,保存寄存器EBX,ESI和EDI的值。
        首先foo必须建立它自己的栈帧。EBP寄存器现在正指向main的栈帧中的某个位置,这个值必须被保留,因此,EBP进栈。然后ESP的内容赋值给了EBP。这使得函数的参数可以通过对EBP附加一个偏移量得到,而栈寄存器ESP便可以空出来做其他事情。如此一来,几乎所有的c函数都由如下两个指令开始:

    push ebp
    mov ebp, esp

        此时的栈如下图所示。在这个场景中,第一个参数的地址是EBP加8,因为main的EBP和返回地址各在栈中占了4个字节。

        下一步,foo必须为它的局部变量分配空间,同时,也必须为它可能用到的一些临时变量分配空间。比如,foo中的一些C语句可能包括复杂的表达式,其子表达式的中间值就必须得有地方存放。这些存放中间值的地方同城被称为临时的,因为他们可以为下一个复杂表达式所复用。为说明方便,我们假设我们的foo中有两个int类型(每个4字节)的局部变量,需要额外的12字节的临时存储空间。简单地把栈指针减去20便为这20个字节分配了空间:

    sub esp, 20

        现在,局部变量和临时存储都可以通过基准指针EBP加偏移量找到了。
        最后,如果foo用到EBX,ESI和EDI寄存器,则它f必须在栈里保存它们。结果,现在的栈如下图所示。 

        foo的函数体现在可以执行了。这其中也许有进栈、出栈的动作,栈指针ESP也会上下移动,但EBP是保持不变的。这意味着我们可以一直用[EBP+8]找到第一个参数,而不管在函数中有多少进出栈的动作。
        函数foo的执行也许还会调用别的函数,甚至递归地调用foo本身。然而,只要EBP寄存器在这些子调用返回时被恢复,就可以继续用EBP加上偏移量的方式访问实际参数,局部变量和临时存储。

6.被调用者返回前的动作

        在把程序控制权返还给调用者前,被调用者foo必须先把返回值保存在EAX寄存器中。我们前面已经讨论过,当返回值占用多于4个或8个字节时,接收返回值的变量地址会作为一个额外的指针参数被传到函数中,而函数本身就不需要返回值了。这种情况下,被调用者直接通过内存拷贝把返回值直接拷贝到接收地址,从而省去了一次通过栈的中转拷贝。
        其次,foo必须恢复EBX,ESI和EDI寄存器的值。如果这些寄存器被修改,正如我们前面所说,我们会在foo执行开始时把它们的原始值压入栈中。如果ESP寄存器指向如图4所示的正确位置,寄存器的原始值就可以出栈并恢复。可见,在foo函数的执行过程中正确地跟踪ESP是多么的重要————也就是说,进栈和出栈操作的次数必须保持平衡。
        这两步之后,我们不再需要foo的局部变量和临时存储了,我们可以通过下面的指令消除栈帧:

    mov esp, ebp
    pop ebp

        其结果就是现在栈里的内容跟上面第2图中所示的栈完全一样。现在可以执行返回指令了。从栈里弹出返回地址,赋值给EIP寄存器。栈如下图所示:

        i386指令集有一条“leave”指令,它与上面提到的mov和pop指令所作的动作完全相同。所以,C函数通常以这样的指令结束:

    leave
    ret

7.调用者在返回后的动作

        在程序控制权返回到调用者(也就是我们例子中的main)后,栈如上图所示。这时,传递给foo的参数通常已经不需要了。我们可以把3个参数一起弹出栈,这可以通过把栈指针加12(=3个4字节)实现:

    add esp, 12

        如果在函数调用前,EAX,ECX和EDX寄存器的值被保存在栈中,调用者main函数现在可以把它们弹出。这个动作之后,栈顶就回到了我们开始整个函数调用过程前的位置,也就是图5中粗线的位置。

        看个具体的实例:

        这段代码反汇编后,代码是什么呢?

#include <stdio.h>
long test(int a, int b){
    a = a + 3;
    b = b + 5;
     return a + b;
}
int main(int argc, char* argv[]){
   printf("%d", test(10,90));
   return 0;
}

        先来看一个概貌:

16:   int main(int argc, char* argv[])
17:   {
00401070   push        ebp
00401071   mov         ebp,esp
00401073   sub         esp,40h
00401076   push        ebx
00401077   push        esi
00401078   push        edi
00401079   lea         edi,[ebp-40h]
0040107C   mov         ecx,10h
00401081   mov         eax,0CCCCCCCCh
00401086   rep stos    dword ptr [edi]
18:        printf("%d",test(10,90));
00401088   push        5Ah
0040108A   push        0Ah
0040108C   call        @ILT+0(test) (00401005)
00401091   add         esp,8
00401094   push        eax
00401095   push        offset string "%d" (0042201c)
0040109A   call        printf (004010d0)
0040109F   add         esp,8
19:        return 0;
004010A2   xor         eax,eax
20:   }

下面来解释一下,

开始进入Main函数  esp=0x12FF84   ebp=0x12FFC0
完成椭圆形框起来的部分:

00401070   push        ebp

ebp的值入栈,保存现场(调用现场,从test函数看,如红线所示,即保存的0x12FF80用于从test函数堆栈返回到main函数):

00401071    mov        ebp,esp

此时ebp=0x12FF80 此时ebp就是“当前函数堆栈”的基址 以便访问堆栈中的信息;还有就是从当前函数栈顶返回到栈底:

00401073     sub        esp,40h 

函数使用的堆栈,默认64个字节,堆栈上就是16个横条(密集线部分)此时esp=0x12FF40。
在上图中,上面密集线是test函数堆栈空间,下面是Main的堆栈空间(补充,其实这个就叫做 Stack Frame):

00401076   push        ebx
00401077   push        esi
00401078   push        edi    入栈 
00401079   lea         edi,[ebp-40h]
0040107C   mov         ecx,10h
00401081   mov         eax,0CCCCCCCCh
00401086   rep stos    dword ptr [edi]   

初始化用于该函数的栈空间为0XCCCCCCCC,即从0x12FF40~0x12FF80所有的值均为0xCCCCCCCC:

18:        printf("%d",test(10,90));
00401088   push        5Ah    参数入栈 从右至左 先90  后10
0040108A   push        0Ah 
0040108C   call        @ILT+0(test) (00401005)    

函数调用,转向eip 00401005 。 
注意,此时仍入栈,入栈的是call test 指令下一条指令的地址00401091下一条指令是add esp,8。
@ILT+0(?test@@YAJHH@Z):

00401005   jmp       test (00401020) 

即转向被调函数test:

8:    long test(int a,int b)
9:    {
00401020   push        ebp
00401021   mov         ebp,esp           
00401023   sub         esp,40h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-40h]
0040102C   mov         ecx,10h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]       //这些和上面一样
10:        a = a + 3;                                    
00401038   mov         eax,dword ptr [ebp+8]     //ebp=0x12FF24 加8 [0x12FF30]即取到了参数
0040103B   add         eax,3
0040103E   mov         dword ptr [ebp+8],eax
11:        b = b + 5;
00401041   mov         ecx,dword ptr [ebp+0Ch]
00401044   add         ecx,5
00401047   mov         dword ptr [ebp+0Ch],ecx
12:        return a + b;
0040104A   mov         eax,dword ptr [ebp+8]
0040104D   add         eax,dword ptr [ebp+0Ch]  //最后的结果保存在eax, 结果得以返回
13:   }
00401050   pop         edi                 
00401051   pop         esi
00401052   pop         ebx
00401053   mov         esp,ebp     //esp指向0x12FF24, test函数的堆栈空间被放弃,从当前函数栈顶返回到栈底
00401055   pop         ebp           //此时ebp=0x12FF80, 恢复现场  esp=0x12FF28
00401056   ret                          ret负责栈顶0x12FF28之值00401091弹出到指令寄存器中,esp=0x12FF30

因为win32汇编一般用eax返回结果 所以如果最终结果不是在eax里面的话 还要把它放到eax。

注意,从被调函数返回时,是弹出EBP,恢复堆栈到函数调用前的地址,弹出返回地址到EIP以继续执行程序。

从test函数返回,执行:

00401091   add         esp,8  

清栈,清除两个压栈的参数10 90 调用者main负责。
(所谓__cdecl调用由调用者负责恢复栈,调用者负责清理的只是入栈的参数,test函数自己的堆栈空间自己返回时自己已经清除)

00401094   push        eax          //入栈,计算结果108入栈,即printf函数的参数之一入栈
00401095   push        offset string "%d" (0042201c)     //入栈,参数 "%d"  当然其实是%d的地址
0040109A   call        printf (004010d0)      //函数调用 printf("%d",108) 因为printf函数时
0040109F   add         esp,8       //清栈,清除参数 ("%d", 108)
19:        return 0;           
004010A2   xor         eax,eax     //eax清零
20:   } 

main函数执行完毕 此时esp=0x12FF34  ebp=0x12FF80:

004010A4   pop         edi
004010A5   pop         esi
004010A6   pop         ebx
004010A7   add         esp,40h    //为啥不用mov esp, ebp? 是为了下面的比较
004010AA   cmp         ebp,esp   //比较,若不同则调用chkesp抛出异常
004010AC   call        __chkesp (00401150)   
004010B1   mov         esp,ebp   
004010B3   pop         ebp          //ESP=0X12FF84  

EBP=0x12FFC0 一切都恢复最初的值了 。

004010B4   ret

另:
1. 如果函数调用方式是__stdcall不同之处在于main函数call 后面没有了add esp, 8;test函数最后一句是 ret 8   (由test函数清栈, ret 8意思是执行ret后,esp+8)。
2. 运行过程中0x12FF28 保存了指令地址 00401091是怎么保存的?栈每个空间保存4个字节(粒度4字节) 例如下一个栈空间0x12FF2C保存参数10,因此:

0x12FF28 0x12FF29 0x12FF2A 0x12FF2B   
   91       10       40       00   

little-endian  认为其读的第一个字节为最小的那位上的数。
3. char a[] = "abcde"  
对局部字符数组变量(栈变量)赋值,是利用寄存器从全局数据内存区把字符串“abcde”拷贝到栈内存中的。
4. int szNum[5] = { 1, 2, 3, 4, 5 }; 栈中是如何分布的?

00401798   mov         dword ptr [ebp-14h],1
0040179F   mov         dword ptr [ebp-10h],2
004017A6   mov         dword ptr [ebp-0Ch],3
004017AD   mov         dword ptr [ebp-8],4
004017B4   mov         dword ptr [ebp-4],5

可以看出来是从右边开始入栈,所以是 5 4 3 2 1 入栈,

int *ptrA = (int*)(&szNum+1);
int *ptrB = (int*)((int)szNum + 1);
std::cout<< ptrA[-1] << *ptrB << std::endl;

结果如何?

28:       int *ptrA = (int*)(&szNum+1);
004017BB   lea         eax,[ebp]
004017BE   mov         dword ptr [ebp-18h],eax

&szNum是指向数组指针;加1是加一个数组宽度;&szNum+1指向移动5个int单位之后的那个地方, 就是把EBP的地址赋给指针;
ptrA[-1]是回退一个int*宽度,即ebp-4;

29:       int *ptrB = (int*)((int)szNum + 1);
004017C1   lea         ecx,[ebp-13h]
004017C4   mov         dword ptr [ebp-1Ch],ecx

如果上面是指针算术,那这里就是地址算术,只是首地址+1个字节的offset,即ebp-13h给指针。实际保存是这样的:

01 00 00 00 02 00 00 00
ebp-14h ebp-13h ebp-10h

注意,是int*类型的,最后获得的是 00 00 00 02,由于Little-endian, 实际上逻辑数是02000000,转换为十进制数就为33554432,最后输出533554432。

8.总结

        根据我的经验,我始终觉得不懂汇编和操作系统原理、不会分析内存数据的C/C++开发不是好的开发,它们属于开发人员的内功。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1514227.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

爆肝整理万能sass框架:react18+webpack5+typescript+ant Design,框架在手,交付无忧!!!

来活了&#xff0c;要求一周时间内快速给xxx业务开发一个sass系统平台&#xff0c;要求有角色权限控制&#xff0c;推荐模块&#xff0c;各种业务内容模块&#xff0c;莫慌&#xff0c;直接上代码&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 1.系统框架配置…

前端跨页面通信的几种方式---同源

参考链接 1、LocalStorage:当 LocalStorage 变化时&#xff0c;会触发storage事件。利用这个特性&#xff0c;我们可以在发送消息时&#xff0c;把消息写入到某个 LocalStorage 中&#xff1b;然后在各个页面内&#xff0c;通过监听storage事件即可收到通知。 2、BroadCast C…

12 list的使用

文档介绍 文档介绍 1.list是可以在常数范围内的任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代 2.list的底层是带头双向链表循环结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前一个元素和…

C++的This详解

目录 听说点赞的UU会有好运哦&#xff01; 听说点赞的UU会有好运哦&#xff01; 听说点赞的UU会有好运哦&#xff01; this指针&#xff1a; 面试常考this&#xff1a; 特别注意&#xff1a; this指针&#xff1a; 类不保存成员函数&#xff0c;而是放在公共代码段&…

Flutter第四弹:Flutter图形渲染性能

目标&#xff1a; 1&#xff09;Flutter图形渲染性能能够媲美原生&#xff1f; 2&#xff09;Flutter性能优于React Native? 一、Flutter图形渲染原理 1.1 Flutter图形渲染原理 Flutter直接调用Skia。 Flutter不使用WebView&#xff0c;也不使用操作系统的原生控件,而是…

【计算机网络实践】FileZilla Server1.8.1实现局域网ftp文件传输

大二新生随便写写笔记&#xff0c;轻喷&#xff0c;鉴于本人在网络搜索中并未搜索到1.8.1版本的使用方法&#xff0c;因而瞎写一页。 一、准备 下载一个FileZilla Server1.8.1在你想作为服务器的主机上&#xff08;此处直接在官网下载即可&#xff1a;Download FileZilla Serve…

stimulsoft report for js vue3使用

项目后端使用的java&#xff0c;试验过积木报表&#xff08;web界面类型的&#xff09;、JasperReport&#xff08;.jasper报表文件&#xff09;、stimulsoft web版本&#xff08;.mrt报表文件&#xff09; 我们的项目是前后端分离的&#xff0c;用积木报表&#xff08;开箱即…

Spring boot 集成netty实现websocket通信

一、netty介绍 Netty 是一个基于NIO的客户、服务器端的编程框架&#xff0c;使用Netty 可以确保你快速和简单的开发出一个网络应用&#xff0c;例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程&#xff0c;例如&#xff1a;基于TCP和U…

【研发日记】,Matlab/Simulink开箱报告(十)——Requirements Toolbox

前言 见《开箱报告&#xff0c;Simulink Toolbox库模块使用指南&#xff08;五&#xff09;——S-Fuction模块(C MEX S-Function)》 见《开箱报告&#xff0c;Simulink Toolbox库模块使用指南&#xff08;六&#xff09;——S-Fuction模块&#xff08;TLC&#xff09;》 见《开…

狂揽Github—start19.7k☆开源OCR—Umi-OCR

文章目录 背景Umi-OCR—源码下载Umi-OCR—可执行程序下载页面介绍截图OCR识别批量OCR识别批量文档二维码全局设置 总结&#xff1a; 背景 大家都知道我是一个Python办公自动化的小小程序员&#xff0c;经常收集一些免费开源的OCR供大家使用&#xff0c;目前我已经写出来多家OCR…

集智书童 | 炸裂 !轻量化YOLO | ShuffleNetv2与Transformer结合,重塑YOLOv7成就超轻超快YOLO

本文来源公众号“集智书童”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;炸裂 &#xff01;轻量化YOLO | ShuffleNetv2与Transformer结合&#xff0c;重塑YOLOv7成就超轻超快YOLO 随着移动计算技术的迅速发展&#xff0c;在移动…

flutter入门

本文真对 Flutter 的技术特性&#xff0c;做了一些略全面的入门级的介绍&#xff0c;如果你听说过Flutter&#xff0c;想去了解它&#xff0c;但是又不想去翻厚厚的API&#xff0c;那么本文就是为你准备的。 随着纯客户端到Hybrid技术&#xff0c;到RN&Weex&#xff0c;再…

【OpenGL手册13】 光照贴图

目录 一、说明二、漫反射贴图三、镜面光贴图四、采样镜面光贴图练习 一、说明 在上一节中&#xff0c;我们讨论了让每个物体都拥有自己独特的材质从而对光照做出不同的反应的方法。这样子能够很容易在一个光照的场景中给每个物体一个独特的外观&#xff0c;但是这仍不能对一个…

C#重新认识笔记_ FixUpdate + Update

C#重新认识笔记_ FixUpdate Update Update: 刷新频率不一致,非物理对象的移动&#xff0c;简单的刷新可用&#xff0c; FixedUpdate: 刷新频率一致,按照固定频率刷新&#xff0c;一般调用FixedUpdate之后&#xff0c;会立即进入必要的物理计算中,因此&#xff0c;任何影响刚…

层次式架构设计

本博客地址&#xff1a;https://security.blog.csdn.net/article/details/136660435 一. 概述 1、软件体系结构为软件系统提供了结构、行为和属性的高级抽象&#xff0c;由构成系统的元素描述这些元素的相互作用、指导元素集成的模式以及这些模式的约束组成。层次式体系结构设…

王庆:当下股市过于悲观,A股、港股基本完成补跌和普跌过程,逆向布局时机已到

核心观点&#xff1a; 1、房地产对中国经济增长拖累最严重的时期正在过去...密切关注真正拐点的出现。 2、当前资本市场从价格表现上来讲&#xff0c;表现的远远超过了基本面所决定的悲观程度。 由于当前资本市场过于悲观&#xff0c;那么反过来就是孕育着机会。 3、我们判…

leetcode刷题(javaScript)——分治思想(二分查找、快速排序)相关场景题总结

分治思想是一种将问题分解成更小的子问题&#xff0c;然后解决子问题并将结果合并的算法设计策略。二分查找、快速排序和折半查找都属于分治思想的经典算法。在leetcode里&#xff0c;分治思想一般结合其他场景出现&#xff0c;构成复合型题目。但是在看题时一定要了解能否用分…

Docker入门二(应用部署、迁移与备份)

文章目录 一、应用部署1.MySQL部署2.Redis部署3.Nginx部署 二、迁移与备份1.容器做成镜像2.把镜像被分成压缩包 一、应用部署 1.MySQL部署 在dokcer中部署mysql&#xff0c;以后不需要在宿主机上装mysql1.做端口映射docker run -id --namemysql5.7 -p 3306:3306 -e MYSQL_ROOT…

九数分三组

枚举三位数时&#xff0c;不用写三个循环&#xff0c;写出最小和最大数循环就行。在这题里要求三个数中不能有重复的数字&#xff0c;先转换为字符串&#xff0c;再转换为字符数组进行排序&#xff0c;最后比较字符串就可以得出结果。这题把结果和原因调换了一下

每日OJ题_牛客HJ37 统计每个月兔子的总数(IO型OJ)

目录 牛客HJ37 统计每个月兔子的总数 解析代码 牛客HJ37 统计每个月兔子的总数 统计每个月兔子的总数_牛客题霸_牛客网 解析代码 #include <iostream> #include <vector>using namespace std; int main() {int n 0;cin >> n;vector<int> arr(n 1…