C语言之反汇编查看函数栈帧的创建与销毁

文章目录

  • 一、 什么是函数栈帧?
  • 二、 理解函数栈帧能解决什么问题呢?
  • 三、 函数栈帧的创建和销毁解析
    • 3.1、什么是栈?
    • 3.2、认识相关寄存器和汇编指令
      • 3.2.1 相关寄存器
      • 3.2.2 相关汇编命令
    • 3.3、 解析函数栈帧的创建和销毁
      • 3.3.1 预备知识
      • 3.3.2 代码和环境搭建
      • 3.3.3 函数栈帧的创建
      • 3.3.4 函数栈帧的销毁
  • 四、总结与开局疑难解答

一、 什么是函数栈帧?

函数栈帧是用于在计算机程序中实现函数调用的一种数据结构。在函数调用过程中,每个函数都需要在内存中创建一个栈帧,用于存储局部变量、返回地址和参数等。

  • 具体来说,函数栈帧通常包含以下部分:

  • 局部变量表:存储函数的局部变量,包括基本数据类型(如整数、浮点数等)和对象引用(如指针)。

  • 返回地址:存储函数的返回地址,即函数执行完毕后需要跳转到的地址。

  • 参数表:存储函数的输入参数,通常按照传递的顺序排列。

  • 操作数栈:用于存储函数的临时数据和中间结果,通常使用栈结构进行操作。

  • 当一个函数被调用时,会在内存中创建一个新的栈帧,并将其压入调用该函数的栈中。当函数执行完毕后,该栈帧会被弹出栈并销毁。因此,函数栈帧在函数调用过程中起到了存储和传递数据的作用。

函数栈帧的实现方式取决于具体的编程语言和编译器。在一些高级编程语言中,编译器通常会为每个函数自动创建和销毁栈帧,而无需程序员手动管理。而在低级编程语言或手动控制内存分配的情况下,程序员需要手动创建和销毁栈帧。

二、 理解函数栈帧能解决什么问题呢?

理解函数栈帧有什么用呢?
只要理解了函数栈帧的创建和销毁,以下问题就能够很好的额理解了:

  • 局部变量是如何创建的?
  • 为什么局部变量不初始化内容是随机的?
  • 函数调用时参数时如何传递的?传参的顺序是怎样的?
  • 函数的形参和实参分别是怎样实例化的?
  • 函数调用是怎么做的?函数的返回值是如何带会的?

让我们一起走进函数栈帧的创建和销毁的过程中。

三、 函数栈帧的创建和销毁解析

3.1、什么是栈?

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。

在这里插入图片描述

  • 在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈(Push):将数据项添加到栈的顶部。这相当于把数据放到栈的最上面。出栈(Pop):从栈的顶部移除数据项。这相当于移除栈顶的数据项。但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像一个桶,先放的东西最后才能拿出
  • 在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。

在经典的操作系统中,栈总是向下增长(由高地址向低地址)的
在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的

3.2、认识相关寄存器和汇编指令

3.2.1 相关寄存器

  • 【eax】:通用寄存器,保留临时数据,常用于返回值
  • 【ebx】 :通用寄存器,保留临时数据
  • 【ebp】:栈底寄存器
  • 【esp】:栈顶寄存器
  • 【eip】:指令寄存器,保存当前指令的下一条指令的地址

3.2.2 相关汇编命令

  • 【mov】:数据转移指令
  • 【push】:数据入栈,同时esp栈顶寄存器也要发生改变
  • 【pop】:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
  • 【add】:加法命令
  • 【sub】:减法命令
  • 【lea】 :load effective address,加载有效地址
  • 【call】:函数调用,1. 压入返回地址 2. 转入目标函数
  • 【jump】:通过修改eip,转入目标函数,进行调用
  • 【ret】:恢复返回地址,压入eip,类似pop eip命令

3.3、 解析函数栈帧的创建和销毁

  • 首先我们达成一些预备知识才能有效的帮助我们理解,函数栈帧的创建和销毁。

3.3.1 预备知识

  • 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间
  • 这块空间的维护是使用了2个寄存器: espebp ,【ebp】 记录的是栈底的地址, esp 记录的是栈顶的地址

如图所示:

在这里插入图片描述

  • 函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2022为例。

3.3.2 代码和环境搭建

  • 这段代码,如果我们在VS2019编译器上调试,调试进入Add函数后,我们就可以观察到函数的调用堆栈
#include <stdio.h>int Add(int x, int y)
{int z = 0;z = x + y;return z;
}int main()
{int a = 10;int b = 20;int c = 0;c = Add(a, b);printf("%d\n", c);return 0;
}
  • 首先我们来做一些环境的搭建工作

在这里插入图片描述

在这里插入图片描述

  • 首先直接在键盘上按下F10【笔记本按下Fn + F10】。

  • 以往写代码的时候,我们都知道要写这么一个main函数,程序就是从这里开始运行的

  • 接下去在按下F10后到监视窗口打开【调用堆栈】的窗口

在这里插入图片描述

  • 然后就出现了这样的界面。此时我们的main函数就从第13行开始运行了

在这里插入图片描述

  • 一直按F10,当调试箭头运行到第【22行】的时候,就会自动进入到exe_common.inl,此时我们就可以观察到底是哪个函数调用了main函数

  • 通过下图可知是invoke_main这个函数调用的,我们了解到这里就可以了~~

在这里插入图片描述

  • 然后,关掉这个【调用堆栈】的窗口后,重新调试起来
  • 调出【反汇编】【内存】【监视】这三个窗口

【反汇编】
在这里插入图片描述
【内存】

在这里插入图片描述

【监视】
在这里插入图片描述

在这里插入图片描述

好,现在我们的环境已经全部搭建好了

3.3.3 函数栈帧的创建

  • 接下去,我们正式开始分析函数栈帧究竟是如何创建的
  • 去掉符号名,方便看内存

在这里插入图片描述

  • 从上图看到已经进入到main函数了
  • main函数是由invoke_main这个函数来进行调用的,所以我们先画出它的函数栈帧

在这里插入图片描述

  • 首先看到左边的两个寄存器【esp】和【ebp】,分别用来维护栈顶和栈顶。
  • 对于栈来说是从【高地址】向【低地址】使用的。

  • 好,接下去的话就要执行第一条指令。将栈中push一个ebp,也就是将ebp中的值进行一个压栈的操作,此时的ebp中存放的是invoke_main函数栈帧的ebp
00EE18D0  push        ebp
  • 随着push入栈的操作,维护栈顶的esp就要往上
    在这里插入图片描述

  • 然后我们看寄存器的变化

在这里插入图片描述

  • 我们再继续执行一下push这句指令,你就会发现【esp】中所存放的地址变小了,原来存的是【ebp】中的值,只是这个存放的形式是倒着存放的,是因为有大小端存储的问题

在这里插入图片描述


  • 接下来第二条,【mov】,我们在上面有讲到过是一个数据转移指令,这条指令的含义就是把esp的值存放到ebp中去
00EE18D1  mov         ebp,esp
  • 此时相当于产生了main函数的【ebp】,这个值就是invoke_main函数栈帧的【esp】,从这里开始就要开始维护main函数的函数栈帧了

在这里插入图片描述

  • 通过VS再来看一下,【ebp】中就会存放【esp】的地址了

在这里插入图片描述
在这里插入图片描述


第三条指令

  • 接下来第三条,sub是一条减法命令,那意思就是让esp中的地址减去一个16进制数字【0xe4】,产生新的esp,此时的esp是main函数栈帧的esp
00EE18D3  sub         esp,0E4h

在这里插入图片描述

  • 此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据以及调试信息

  • 通过图,此时你也可以认为【esp】指向了低地址的一块空间
    在这里插入图片描述

  • 来看一下寄存器中存放的内存变化

在这里插入图片描述
在这里插入图片描述


第四、五、六条指令

00EE18D9  push        ebx   //将寄存器ebx的值压栈,esp-4
00EE18DA  push        esi   //将寄存器esi的值压栈,esp-4
00EE18DB  push        edi   //将寄存器edi的值压栈,esp-4
  • 上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复
  • 那随着寄存器的入栈,维护栈顶的寄存器也将发生变化
  • 此时esp也随着压栈而变化

在这里插入图片描述

  • 到VS里来看一下三次的变化:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


第七、八、九、十条指令

  • 下面的代码是在初始化main函数的栈帧空间,【非常重要】
00EE18DC  lea         edi,[ebp-24h]   
00EE18DF  mov         ecx,9  
00EE18E4  mov         eax,0CCCCCCCCh  
00EE18E9  rep stos    dword ptr es:[edi] 

上面的这段代码最后4句,等价于下面的伪代码:

edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{*(int*)edi = eax;
}
  • 首先要来看的就是【lea】就是我们在上面讲到过的【load effective address】加载有效地址的意思,那也就是从【ebp】这个维护栈顶的寄存器减去24h的位置,加载到寄存器【edi】里面去

在这里插入图片描述

  • 然后再将9放到【ecx】中去;以及将【0CCCCCCCCh】这块地址存到【eax】中去;

  • 从【edi】所存放的这块地址的开始,每次初始化4个字节的数据,dword值就是4个字节的大小

  • 这4句话的操作就是从edi开始,每次初始化4个字节的数据,总共初始化ecx次,初始化的内容为【0xCCCCCCCC】,总共初始化到ebp的地址结束

在这里插入图片描述
在这里插入图片描述

  • 到这里,main函数才刚刚被初始化完成
  • 那么里面的cccccccc是初始化的什么内容呢?–>我们来看一下
char arr[20];
printf("%s",arr);

在这里插入图片描述

  • 可以看到上面的程序输出“烫烫烫烫烫烫烫烫烫烫”这一串,是因为main函数调用时,在栈区开辟的空间的其中每一个字节都被初始化为0xCC,上图中arr数组是一个未初始化的数组,恰好在这块空间上创建的,0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”,烫烫烫就这么来的

第十一、十二、十三条指令

  • 我们开始初始化三个变量,每条指令对应上一条代码
	int a = 10;
00EE18F5  mov         dword ptr [ebp-8],0Ah  int b = 20;
00EE18FC  mov         dword ptr [ebp-14h],14h  int c = 0;
00EE1903  mov         dword ptr [ebp-20h],0
  • 其中【mov】是数据转移指令,也就是是将10这个值【ebp - 8】这块地址上

  • 为什么说0Ah就是10呢?因为0Ah是10的十六进制表示形式,在十六进制中A值得就是10

  • 对于14h的话就是16 * 1 + 4 = 20,那就是将20这个值放到【ebp - 14】这块地址上去

  • 最后一句就是将0这个值放到【ebp - 20】这块地址上去

  • 对于为什么-8,-14,-20呢,这是取决于编译器本身的,我是用的是VS2022,可能你到其他编译器上就不一样了

  • 这就可以得出一个结论:我们所定义的变量在栈内存中并不是呈现一个连续存放的,可能是分散的

  • 接下去继续来看这三次的存放值的变化~~

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 我们再来看图,也将这些画出来。

在这里插入图片描述


第十四、十五、十六、十七条指令

  • 此时main函数中的变量创建好了,那就要调用Add函数了
00EE190A  mov         eax,dword ptr [ebp-14h]  
00EE190D  push        eax  
00EE190E  mov         ecx,dword ptr [ebp-8]  
00EE1911  push        ecx
  • 来看第一条,将【ebp-14h】这块地址的内容放到寄存器【eax】中去,那这个时候你就会想到这个【ebp-14】是刚才放数值20,然后压栈。
  • 第三条就是将【ebp-8】中的内容放到寄存器【ecx】中去,它【ebp-8】的地方存放的就是我们刚才放10的地方,然后压栈。

在这里插入图片描述

  • 这样就可以看出,这两个变量相当于实参的一份临时拷贝,这里就回到我们前面学的函数的形参就是实参的一份临时拷贝

再来到VS中看看

在这里插入图片描述

第十八条指令

00EE1912  call        00EE10B9
  • 对于这条【call】指令而言,比较特殊,它有两个作用

①压入返回地址
②转入目标函数

  • 这里就是要压的是 call指令的下一条地址
00EE1917  //这条就是要压入的地址
  • 然后我们来在vs中看一下,当运行到图中的那条语句的时候就要按F11,不能按F10,和调试一个道理

在这里插入图片描述

  • 把这块地址压入栈中
    在这里插入图片描述

第十九、二十、二一条指令

  • 到19条指令开始,就进入Add函数了,这里的函数前面和在main函数中的前面也是非常的相似
  • 所以这个就是在开辟栈帧
00EE1790  push        ebp  
00EE1791  mov         ebp,esp  
00EE1793  sub         esp,0CCh  
00EE1799  push        ebx  
00EE179A  push        esi  
00EE179B  push        edi
  • 首先来看第一条指令。也就是将之前的【ebp】栈底寄存器的值压入到栈顶中
00EE1790  push        ebp  
  • 对于此处的【ebp】,自从它在维护main函数的栈底后就没有再动过来,所以这里push上来的就是main函数的【ebp】

在这里插入图片描述

00EE1791  mov         ebp,esp
  • 接着再来看第二条,也就是将main函数的【esp】重新赋给【ebp】,这里要注意了,不要搞混,此时的【ebp】应该算是在维护Add函数的栈底了

在这里插入图片描述

  • 于是,栈就变成了这样:

在这里插入图片描述

00EE1793  sub         esp,0CCh
  • 接着第三条,【sub】命令使得【esp】存放的地址块减去一个CC的大小,继续结合上面那条指令,此时Add函数的栈顶和栈底都被找到了

在这里插入图片描述

  • 此时就相当于是在做一个迭代的操作

在这里插入图片描述


第二二、二三、二四条指令

00EE1799  push        ebx  
00EE179A  push        esi  
00EE179B  push        edi
  • 接下去还是一样的三条压栈操作
  • 来到VS中观看【esp】的变化

在这里插入图片描述

  • 接着将这三个寄存器压入栈

在这里插入图片描述

第二五、二六、二七、二八条指令

  • 对于这四条指令和上面main函数的创建过程类似,便不做不过分析
00EE179C  lea         edi,[ebp-0Ch]  
00EE179F  mov         ecx,3  
00EE17A4  mov         eax,0CCCCCCCCh  
00EE17A9  rep stos    dword ptr es:[edi]
  • 继续到VS中观看的变化

在这里插入图片描述

第二十九条指令

  • 接下去我们进入第二十九条指令,也就是对Add函数中存放计算总和的变量z进行初始化操作。
  • 【mov】做数据转移,将0放到【ebp-8】这块地址上去
	int z = 0;
00EE17B5  mov         dword ptr [ebp-8],0 

在这里插入图片描述

  • 然后我们在Add的栈帧中初始化这个变量z
    在这里插入图片描述

第三十、三十一、三十二条指令

  • 接下去的三条指令就是对两个形参的值进行一个相加
	z = x + y;
00EE17BC  mov         eax,dword ptr [ebp+8]  
00EE17BF  add         eax,dword ptr [ebp+0Ch]  
00EE17C2  mov         dword ptr [ebp-8],eax
  • 那么上面不是只初始化了一个变量z吗,变量x和变量y在哪里呢?
  • 我们之前有做过了一步操作,也就是将这两个实参的拷贝进行了一个压栈操作,那时就说了对于这个就是形参
00EE190A  mov         eax,dword ptr [ebp-14h]  
00EE190D  push        eax  
00EE190E  mov         ecx,dword ptr [ebp-8]  
00EE1911  push        ecx
  • 此时我们就要通过这三句指令去找回这两个形参的值,关键的就是【ebp+8】和【ebp+0Ch】。因为我们在入栈的时候【ebp】寄存器存放的地址都是逐渐变小的,因为 栈是从高地址往低地址生长的,所以我们要去找回之前压入的内容,就要把地址加回去
  • 如下图所示

在这里插入图片描述

  • 找到这两个值之后,首先将【10】放到【eax】寄存器中去,然后再将【20】在加到寄存器【eax】原有的值上去,此时【eax】中存放的便是【30】

在这里插入图片描述

  • 注意看寄存器【eax】的变化

在这里插入图片描述

  • 这里还可以直接到指令这里来看。直接将鼠标放到【z】上面就可以看到了

在这里插入图片描述

  • 然后再将计算出来存放在【eax】中的值再放回【ebp-8】这块地址上去
00EE17C2  mov         dword ptr [ebp-8],eax
  • 首先到VS中来看看变化

在这里插入图片描述

  • 然后修改一下之前Add函数栈帧中存放z的内容

在这里插入图片描述

第三十三条指令

  • z计算出来了,此时就要执行【return z】这句代码,将z返回给main函数,但是函数栈帧中可不是这么做的
	return z;
00EE17C5  mov         eax,dword ptr [ebp-8]
  • 看上面的指令可以看到,是将【ebp-8】中的内容转存到寄存器【eax】中去
  • 从【eax】~【ebx】这些寄存器都可以用来存放临时数据,并不是说上一次用过了就不能再用了,这其实和我们在定义一个变量后进行反复使用是一个道理。
  • 然后在Add函数调用结束后,它所对应的函数栈帧就会被销毁,此时被创建出来的临时变量【z】就不复存在了,因为【z】也是存放在Add的函数栈帧中的,所以这一步的操作其实就是将我们在Add函数中计算出来的值给保存起来,因为寄存器而言程序没有结束的话它是不会被销毁的,我们后面还可以到这个寄存器中去取数据

3.3.4 函数栈帧的销毁

接下去要进行的就是函数栈帧的销毁操作

第三十四、三十五、三十六条指令

  • 接下来就是三条pop的指令,也就是在栈顶弹出对应的值,然后放到对应的寄存器中去
00EE17C8  pop         edi      //在栈顶弹出一个值,存放到edi中,esp+4
00EE17C9  pop         esi     //在栈顶弹出一个值,存放到esi中,esp+4
00EE17CA  pop         ebx     //在栈顶弹出一个值,存放到ebx中,esp+4
  • 我们先到VS中来看看

在这里插入图片描述

  • 通过图示来看一下
    在这里插入图片描述

第三十七条指令

  • 当给Add函数开辟函数栈帧的时候,最后一步是把【esp】中存放的内容给到【ebp】,也就是相当于就是让【ebp】指向和【esp】的同一块空间
  • 下面这句指令就是将【ebp】中存放的内容给到【esp】,那其实就是让【esp】指向和【ebp】的同一块空间
00EE17D8  mov         esp,ebp
  • 通过图示来看一下

在这里插入图片描述

  • 到VS中来看一下

在这里插入图片描述

第三十八条指令

00EE17DA  pop         ebp
  • 这句指令很重要,因为此时Add函数的函数栈帧已经被销毁了,此时我们要回到main函数的函数栈帧,那么两个维护栈顶和栈底的寄存器就要发生变化,此时我们要pop的【ebp】是之前压栈进来的main函数的ebp
  • pop的作用:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
  • pop了之后【esp】也要发生一个变化

在这里插入图片描述

  • 到VS中再来看一下变化。此时不要混淆了,栈是从高地址往低地址增长的,所以栈底的地址来的大一些

在这里插入图片描述

第三十九条指令

  • 这里只有一个【ret】,这个指令会从栈顶弹出一个值,那这个时候从上图其实可以看到此时的【esp】栈顶寄存器指向的这块地址,这块地址是call指令的下一条指令地址,就是我们在进入Add函数前提前压入的地址
00EE17DB  ret
  • 此时就会直接跳转到call指令下一条指令的地址处,继续往下执行

在这里插入图片描述

  • 再来看看【esp】的变化

在这里插入图片描述

第四十条指令

  • 有的同学看到的就是一个【esp】的变化,【add】是加法命令,也就是将【esp】的位置加上一个8,一块内存空间是4,加8的话那此时【esp】是不是就来到了【edi】的位置
  • 这其实就是在【销毁Add函数的函数形参x,y】,这下你应该明白函数形参是在什么时候销毁的了吧,没错,就是从Add函数回到main函数之后
0046185D 83 C4 08      add      esp,8
  • 我们来看看示意图:

在这里插入图片描述

  • 一样,VS也来看看【esp】的变化

在这里插入图片描述

第四十一条指令

00EE191A  mov         dword ptr [ebp-20h],eax
  • 将eax中值,存档到ebp-0x20的地址处,其实就是存储到main函数中ret变量中,而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的。

  • 先前在Add函数中计算出来的30,首先放到【eax】寄存器中保存起来,现在过来好几条指令后,它还保存在里面,我们只需要使用【mov】将数据做一个转移即可

  • 到VS里来看看变化

在这里插入图片描述

  • 最后main函数栈帧的销毁也同理,这里就不再介绍了

  • 以下是这个栈的全局浏览图
    在这里插入图片描述

拓展了解:

其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。具体可以参考《程序员的自我修养》一书的第10章。

到这里我们给大家完整的演示了main函数栈帧的创建,Add函数站真的额创建和销毁的过程,相信大家已经能够基本理解函数的调用过程,函数传参的方式,也能够回答最开始的问题了

四、总结与开局疑难解答

① 局部变量是如何创建的?

  • 首先为函数分配好栈帧空间,将这块栈帧空间初始化好后,然后给局部在栈帧里分配空间

② 为什么局部变量不初始化内容是随机的?

  • 因为函数栈帧中的空间是预先初始化好的【0xCCCCCCCCh】,若是不为变量初始化内容,那使用的就是初始化好后的内容,以字符的形式打印出来便是烫烫烫烫烫烫

③ 函数调用时参数时如何传递的?传参的顺序是怎样的?

  • 当还没有进入函数的时候,就已经将函数实参做了一份临时拷贝,并从右向左压入栈中【FILO】,当真正进入到函数栈帧中时,通过指针的偏移量,就可以顺着找回来,找到这份临时拷贝的形参

④ 函数的形参和实参分别是怎样实例化的?

  • 形参确实是我在压栈的时候开辟的一块空间,它和实参只是值相同,但是空间是独立的,所以形参是实参的一份临时拷贝,改变形参的值不会影响到实参

⑤ 函数调用是怎么做的?返回值是如何带会的?

  • 当执行到【call】指令的时候,把call指令的下一条指令地址压入栈中,相当于记住了这个地址。接着进入到函数中,当函数执行结束的时候,回到主函数中,再执行【ret】指令就可以回到call指令的下一条指令地址
  • 返回值是通过寄存器带回来的、将函数中计算出来的返回值存放到寄存器中,因为寄存器不会随着函数的调用结束而被销毁,最后再将寄存器中存放的数据转存回对应的内存块中即可

好了,函数的栈帧的创建与销毁所有内容就到这里就结束了
如果有什么问题可以私信我或者评论里交流~~
感谢大家的收看,希望我的文章可以帮助到正在阅读的你🌹🌹🌹

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

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

相关文章

AI对比:ChatGPT和文心一言的区别和差异

目录 一、ChatGPT和文心一言大模型的对比分析 1.1 二者训练的数据情况分析 1.2 训练大模型数据规模和参数对比 1.3 二者3.5版本大模型对比总结 二、ChatGPT和文心一言功能对比分析 2.1 二者产品提供的功能情况分析 2.2 测试一下各种功能的特性 2.2.1 文本创作能力 2.2…

婴儿洗衣机怎么选?热门品牌希亦、觉飞、由利详细测评

宝宝的衣物是不是要和大人的衣服分开洗呢&#xff1f;这是很多新手爸妈都会遇到的一个问题。有的人认为&#xff0c;宝宝的衣服要单独洗&#xff0c;以免被大人的衣服上的细菌污染&#xff1b;有的人认为&#xff0c;宝宝的衣服可以和大人的衣服一起洗&#xff0c;这样可以节省…

优先级队列(堆)

目录 1 概念 2 堆的概念 2.1小根堆 2.2大根堆 3堆的存储方式​​​​​​​ 4、堆的创建 4.1堆向下调整 5、时间复杂度 6、堆的插入&#xff08;向上调整&#xff09; 7、堆的删除 8、PriorityQueue的特性 9、堆排序 1 概念 我们知道的队列&#xff0c;队列是一…

leetcode---Z字形变换

题目&#xff1a; 将一个给定字符串 s 根据给定的行数 numRows &#xff0c;以从上往下、从左到右进行 Z 字形排列。比如输入字符串为 "PAYPALISHIRING" 行数为 3 时&#xff0c;排列如下&#xff1a;之后&#xff0c;你的输出需要从左往右逐行读取&#xff0c;产生…

redis高可用之主从部署

文章目录 前言1. 同步以及命令传播1.1 同步1.2 命令传播 2. 解决从服务器断线重连2.1 解决方案 3. PSYNC命令4. 复制步骤1:设置主服务器的地址和端口步骤2:建立套接字连接 ——其实就是建立TCP连接步骤3:发送PING命令步骤4:身份验证步骤5:发送端口信息步骤6:同步步骤7:命令传播…

鸿蒙5.0发布时间已定!何处寻得移动开发加速器?

直接在百度上搜索「鸿蒙5.0发布时间」&#xff0c;出来的结果&#xff0c;那一个比一个焦虑~~ 百度的AI基于综合内容判断得出&#xff0c;鸿蒙5.0的发布时间在2023-04-17 百度知道推的答案是202年年4月中 但不管几月&#xff0c;“鸿蒙元年”似乎都是确定的&#xff0c;就是…

Linux切换jdk版本

参考文献&#xff1a;Linux 多个JDK的版本 脚本切换 - C小海 - 博客园 (cnblogs.com)

ZYNQ-7020 集成了运行NI Linux Real‑Time的实时处理器,支持FPGA二次开发

模拟和数字I/O&#xff0c;667 MHz双核CPU&#xff0c;512 MB DRAM&#xff0c;512 MB存储容量&#xff0c;Zynq-7020 FPGA CompactRIO Single-Board控制器 sbRIO‑9637是一款嵌入式控制器&#xff0c;在单块印刷电路板(PCB)上集成了运行NI Linux Real‑Time的实时处理器、用户…

RK3568 移植Ubuntu

使用ubuntu-base构建根文件系统 1、到ubuntu官网获取 ubuntu-base-18.04.5-base-arm64.tar.gz Ubuntu Base 18.04.5 LTS (Bionic Beaver) 2、将获取的文件拷贝到ubuntu虚拟机,新建目录,并解压 mkdir ubuntu_rootfs sudo tar -xpf u

解密高压开关柜内温度感知神器——无线测温传感器

具长期电网运行数据表明&#xff0c;电网电气设备故障大多是由于大电流运行、设备老化、绝缘水平下降等原因导致设备在高温条件下运行&#xff0c;从而引发燃烧&#xff0c;爆炸等严重事故。因此准确的掌握电气设备温度是预防此类问题的关键。 开关柜无源无线测温传感器采用CT取…

virtualenv虚拟环境的安装使用教程

让我们先思考这样一种情景&#xff1a;我们用python来开发一个项目&#xff0c;那么这个项目肯定会依赖很多的第三方库&#xff0c;这些第三方的库通过pip安装到全局区当中&#xff0c;而对于不同的项目使用到的库可能都有所不同&#xff0c;但是这些项目的库都安装到全局区当中…

【JavaEE进阶】MyBatis⼊⻔

文章目录 &#x1f332;什么是MyBatis?&#x1f333;准备⼯作&#x1f6a9;创建⼯程&#x1f6a9;数据准备&#x1f6a9;配置数据库连接字符串&#x1f6a9; 在项⽬中,创建持久层接⼝UserInfoMapper &#x1f343;单元测试&#x1f6a9;使⽤Idea⾃动⽣成测试类 &#x1f340;打…

6 时间序列(不同位置的装置如何建模): GRU+Embedding

很多算法比赛经常会遇到不同的物体产生同含义的时间序列信息&#xff0c;比如不同位置的时间序列信息&#xff0c;风力发电、充电桩用电。经常会遇到该如此场景&#xff0c;对所有数据做统一处理喂给模型&#xff0c;模型很难学到区分信息&#xff0c;因此设计如果对不同位置的…

芯课堂 | SWM34S系列驱动TFT-LCD显示模组应用基本注意事项

1、确认硬件的连接、包括电源、地、RGB 数据线、DCLK\DE\HSYNC\VSYNC 等&#xff0c;显示模组有 DISP、RESET、CS、SCL、SDA 等。 2、确认各电压的正常&#xff0c;包括电源&#xff0c;部分有 IOVCC、VGL、VGH、VCOM 等电压 3、如果应用的 TFT-LCD 模组非演示例程中已适配调…

动态血糖监测市场调研:预计2029年将达到13亿美元

血糖监测即是对于血糖值的定期检查。实施血糖监测可以更好的掌控糖尿病患者的血糖变化&#xff0c;对生活规律&#xff0c;活动&#xff0c;运动&#xff0c;饮食以及合理用药都具有重要的指导意义&#xff0c;并可以帮助患者随时发现问题&#xff0c;及时到医院就医。 动态血糖…

LinkedList源码

LinkedList源码 总结 LinkedList数据结构采用链表&#xff0c;内部封装了Node类&#xff0c;set方法先让Node的pre指针指向之前的last节点&#xff0c;然后判断头节点知否为空&#xff0c;如果为空则让first指针指向该节点&#xff0c;不过不为空则让尾节点的next指针指向该节…

Linux:动静态库的概念制作和底层工作原理

文章目录 动静态库基础认知动静态库基本概念静态库的制作库的概念包的概念 静态库的使用第三方库小结 动态库的制作动态库的使用动态库如何找到内容&#xff1f;小结 动态库加载库和程序都要加载可执行程序的地址问题地址问题逻辑地址和平坦模式绝对编址和相对编址与位置无关码…

vue2(Vuex)、vue3(Pinia)、react(Redux)状态管理

vue2状态管理Vuex Vuex 是一个专为 Vue.js应用程序开发的状态管理模式。它使用集中式存储管理应用的所有组件的状态&#xff0c;以及规则保证状态只能按照规定的方式进行修改。 State&#xff08;状态&#xff09;:Vuex 使用单一状态树&#xff0c;即一个对象包含全部的应用层…

高防IP如何保护服务器

首先我们要知道什么是高防IP~ 高防IP是指高防机房所提供的ip段&#xff0c;主要是针对互联网服务器遭受大流量DDoS攻击时进行的保护服务。高防IP是目前最常用的一种防御DDoS攻击的手段&#xff0c;用户可以通过配置DDoS高防IP&#xff0c;将攻击流量引流到高防IP&#xff0c;防…

[pytorch入门] 3. torchvision中的transforms

torchvision中的transforms 是transforms.py工具箱&#xff0c;含有totensor、resize等工具 用于将特定格式的图片转换为想要的图片的结果&#xff0c;即用于图片变换 用法 在transforms中选择一个类创建对象&#xff0c;使用这个对象选择相应方法进行处理 能够选择的类 列…