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指令的下一条指令地址
  • 返回值是通过寄存器带回来的、将函数中计算出来的返回值存放到寄存器中,因为寄存器不会随着函数的调用结束而被销毁,最后再将寄存器中存放的数据转存回对应的内存块中即可

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python 猎户星空Orion-14B,截止到目前为止,各评测指标均名列前茅,综合指标最强;Orion-14B表现强大,LLMs大模型

1.简介 Orion-14B-Base是一个具有140亿参数的多语种大模型&#xff0c;该模型在一个包含2.5万亿token的多样化数据集上进行了训练&#xff0c;涵盖了中文、英语、日语、韩语等多种语言。在多语言环境下的一系列任务中展现出卓越的性能。在主流的公开基准评测中&#xff0c;Orio…

Qt —— 自定义飞机仪表控件(附源码)

示例效果 部署环境 本人亲测版本Vs2017+Qt5.12.4,其他版本应该也可使用。 源码1 qfi_ADI::qfi_ADI( QWidget *parent ) :QGraphicsView ( parent ),m_scene ( nullptr )

英语连读技巧12

1. this thing – 这件事 连读听起来就像是&#xff1a;【the sing】 连读的音标为&#xff1a; 例句&#xff1a;i cannot get this thing off my mind. 发音指导&#xff1a;注意 “this” 和 “thing” 两个词在连读时&#xff0c;“s” 和 “th” 的音接近融合&#xf…

浏览器提示“此网站的安全证书有问题”

有时候在浏览一些网站的时候&#xff0c;点进去的时候不是直接显示内容&#xff0c;而是弹出来一个警告的界面&#xff0c;告诉你此网站的安全证书有问题&#xff0c;浏览器是建议你不要再继续访问了&#xff0c;当然你也可以选择继续访问。那么&#xff0c;你还敢继续访问一个…

2.1.2 关系模式

1. 什么是关系模式 2. 定义关系模式 3. 关系模式与关系 1&#xff0e;什么是关系模式 关系模式是型&#xff0c;关系是值 。 关系模式是对关系的描述&#xff1a; 描述关系元组集合的结构 &#xff0c; 属性构成 l&#xff0c;属性来自的域 l&#…

品牌营销:长期价值与潜在利益的共赢之路

在当今品牌营销面临着前所未有的挑战。品牌信息的传递不仅需要迅速&#xff0c;更要持久。而在这个过程中&#xff0c;产品的长期价值和潜在利益成为品牌营销的核心。本文迅腾文化将通过奥卡姆剃刀定律和霍夫曼编码的视角&#xff0c;探讨品牌营销如何实现长期价值和潜在利益的…

gitlab.rb主要配置

根据是否docker安装,进入挂载目录或安装目录 修改此文件,我一般是在可视化窗口中修改,有时候也在命令行手敲 将下面的配置复制到该文件中 external_url http://192.168.100.50 # nginx[listen_port] = 8000 (docker安装的这一行不需要,因为端口映射导致此处修改会导致访问…

助力公益事业,吉林长春市第二社会福利院与清雷科技达成合作

“通过部署清雷科技智慧康养大屏和毫米波雷达监测设备&#xff0c;可以实时查看全院入住人员的生命体征情况&#xff0c;包括呼吸、心率、在离床状态、睡眠报告等&#xff0c;对呼吸异常、跌倒风险异常的人员还会特别标注提醒&#xff0c;提高护理员工作效率的同时&#xff0c;…

SCT9430TVBR:3.8V-36V输入,3A同步降压DCDC转换器

• 通过开关节点无振铃降低 EMI • 400KHz固定开关频率&#xff0c;6% 抖频拓展频谱 • 轻载条件下的脉冲跳跃模式 PSM • 3.8V-36V 宽输入电压范围 • 最大连续3A输出负载 • 0.8V 1% 反馈参考电压 • 集成80mΩ (Rdson) 上端MOSFET 和 42mΩ (Rdson) 下端MOSFET • 1uA 关断电…

【机组】指令控制模块实验的解密与实战

​&#x1f308;个人主页&#xff1a;Sarapines Programmer&#x1f525; 系列专栏&#xff1a;《机组 | 模块单元实验》⏰诗赋清音&#xff1a;云生高巅梦远游&#xff0c; 星光点缀碧海愁。 山川深邃情难晤&#xff0c; 剑气凌云志自修。 ​ 目录 &#x1f33a;一、 实验目…

Redis——RDB持久化

前言 Redis是一个键值对数据库服务器&#xff0c;服务器中通常包含任意个非空数据库&#xff0c;而每个非空数据库中又可以包含任意个键值对&#xff0c;为了方便起见&#xff0c;我们将服务器中的非空数据库以及它们的键值对统称为数据库状态。 因为Redis数据库是内存数据库&a…

如何在Docker上运行Redis

环境: 1.windows系统下的Docker deckstop 1.Pull Redis镜像 2.运行Redis镜像 此时,Redis已经启动&#xff0c;我们登录IDEA查看下是否连接上了 显示连接成功&#xff0c;证明已经连接上Docker上的Redis了

INTEWORK—PET 汽车软件持续集成平台

产品概述 INTEWORK-PET-CI是经纬恒润自主研发的汽车软件持续集成&持续交付平台&#xff0c;在传统的持续集成基础上深化了研运一体化&#xff08;DevOps&#xff09;的概念&#xff0c;将嵌入式软件中的拉取代码、检查、构建、测试、版本管理以及发布交付等环节串联起来&am…

大模型学习与实践笔记(十一)

一、使用OpenCompass 对模型进行测评 1.环境安装&#xff1a; git clone https://github.com/open-compass/opencompass cd opencompass pip install -e . 当github超时无法访问时&#xff0c;可以在原命令基础上加上地址&#xff1a; https://mirror.ghproxy.com git clon…

〔保姆级教学〕2024华数杯国际赛B题完整参考论文22页+四小问matlab代码+数据集+可视化高清图表

光伏发电 一、问题分析&#xff08;完整资料在文末&#xff09; 问题一&#xff1a; 首先题目要求得到电能行业与经济状况、居民消费水平、城市化率和市场化程度等因素的关系&#xff0c;并对供电量进行预测。其中&#xff0c;电能采用供电量数据、经济采用GDP数据&#xff…

JavaEE 网络原理

JavaEE 网络原理 文章目录 JavaEE 网络原理1. 网络互连1.1 局域网LAN1.2 广域网WAN 2. 网络通信基础2.1 IP地址2.2 端口号 3. 网络协议3.1 概念3.2 五元组3.3 协议分层3.4 TCP/IP 五层模型3.5 封装和分用 1. 网络互连 随着时代的发展&#xff0c;需要多个计算机协同工作来完成…

一款自动化提权工具

免责声明 请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;作者不为此承担任何责任。工具来自网络&#xff0c;安全性自测&#xff0c;如有侵权请联系删除。…

Unity3D控制人物移动的多种方法

系列文章目录 unity知识点 文章目录 系列文章目录前言一、人物移动之键盘移动1-1、代码如下1-2、效果 二、人物移动之跟随鼠标点击移动2-1、代码如下2-2、效果 三、人物移动之刚体移动3-1、代码如下3-2、效果 四、人物移动之第一人称控制器移动4-1、代码如下4-2、效果 五、And…

Java缓存介绍

一、什么是缓存 1、Cache是高速缓冲存储器 一种特殊的存储器子系统&#xff0c;其中复制了频繁使用的数据以利于快速访问 2、凡是位于速度相差较大的两种硬件/软件之间的&#xff0c;用于协调两者数据传输速度差异的结构&#xff0c;均可称之为 Cache 二、缓存的分类 1、基于…

全文干货!信息化和数字化的本质区别是什么?

信息化和数字化都是行业的发展方向&#xff0c;但有一些区别。 简单来说就是&#xff0c;信息化侧重系统建设&#xff0c;用以管理生成的信息与数据&#xff0c;通常包括建立OA办公系统、业务系统、财务管理系统、客户关系管理系统和人力管理系统等。数字化侧重于将物理业务和…