深入解析函数栈帧创建与销毁

目录

一、函数栈帧(Stack Frame)整理

1、核心概念

2、为什么需要函数栈帧?

3、函数栈帧的主要内容

二、理解函数栈帧能解决的核心问题

1、局部变量的生命周期与本质

2、函数调用的参数传递机制

3、函数返回值的传递

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

1、什么是栈(Stack)?

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

a) 相关寄存器

b) 相关汇编指令

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

1. 预备知识

2. 函数的调用堆栈

3. 准备环境

4. 转到反汇编

5. 函数栈帧的创建

小知识:烫烫烫~

Add函数的传参

函数调用过程

6. 函数栈帧的销毁

拓展了解


一、函数栈帧(Stack Frame)整理

1、核心概念

        函数栈帧(也称为活动记录)是函数被调用时,在程序的调用栈(Call Stack) 上为其分配的一块内存空间。它用于支持函数的执行和管理函数调用过程。

2、为什么需要函数栈帧?

        C程序以函数为基本单位。当一个函数调用另一个函数时,需要解决以下几个关键问题,而函数栈帧正是解决这些问题的机制:

  • 函数参数如何传递?

  • 函数内部的局部变量如何存储?

  • 函数调用结束后,如何返回到正确的位置继续执行?

  • 函数的返回值如何传递给调用者?

  • 函数执行前后,如何保证调用者寄存器的状态不被破坏?

3、函数栈帧的主要内容

一块函数栈帧通常包含以下几类信息:

  • 函数参数与返回值:存储传递给被调用函数的参数以及函数返回时的返回值。

  • 临时变量

    • 函数的非静态局部变量

    • 编译器自动生成的其他临时变量

  • 上下文信息

    • 调用函数的返回地址(调用指令下一条指令的地址)。

    • 调用函数的栈帧基地址,用于在当前函数返回后恢复调用者的栈帧。

    • 为保持函数调用前后不变而需要保存的寄存器值


二、理解函数栈帧能解决的核心问题

        深入理解函数栈帧的创建和销毁过程,就像是获得了C语言函数底层工作机制的“地图”,许多令人困惑的语法现象和编程难题都会变得清晰明了。具体来说,它能帮助我们彻底理解以下关键问题:

1、局部变量的生命周期与本质

  • 局部变量是如何创建的?:它们并非凭空产生,而是在其所属函数的栈帧被创建时,在栈上分配了内存空间。所谓的“创建”,就是移动栈指针来预留一块足够大的内存。

  • 为什么局部变量不初始化内容是随机的?:因为“创建”仅仅是在栈上分配了空间,而这片空间之前很可能被其他函数使用过,残留着之前的数据。如果不主动初始化(赋值),直接使用该内存的值,看到的自然就是不可预测的“随机值”或“垃圾值”。

2、函数调用的参数传递机制

  • 函数调用时参数是如何传递的?:通常不是在被调用函数的栈帧里直接“变出”参数。而是由调用者将自己的实参(的值或地址)压入栈中(或存入约定的寄存器)。随后,被调用函数才能在自己的栈帧中找到这些参数。

  • 传参的顺序是怎样的?:理解栈帧可以清楚地看到,参数通常是从右向左依次压入栈中(超重要!!!)。这是为了支持像printf这样的可变参数函数。

  • 形参和实参的关系形参其实就是函数栈帧中为参数预留的位置。在函数被调用时,实参的值会被拷贝(复制) 到形参所在的内存中。因此,形参是实参的一份副本,修改形参(在大多数情况下)不会影响实参,这解释了为何值传递是有效的。

3、函数返回值的传递

  • 函数的返回值是如何带回的?:通常,返回值不会通过栈帧的主要部分传递。而是通过一个特定的寄存器(如eax/rax)来存储并带回给调用者。如果返回值较大,可能会采用调用者预先分配空间并传入地址等其他机制。

        总结而言,理解函数栈帧就是将编程语言中“函数调用”这个抽象概念,转化为CPU和内存中“分配空间、拷贝数据、跳转指令、恢复现场”等一系列具体操作的过程。这是连接高级语言语法和计算机底层逻辑的关键桥梁。让我们一同深入分析函数栈帧创建和销毁的详细过程。


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

1、什么是栈(Stack)?

        栈是现代计算机程序中一个至关重要的基础概念。它支撑着函数调用、局部变量管理等核心功能,可以说没有栈,就没有我们现在看到的高级编程语言。

栈被定义为一个遵守 “后进先出”(Last In First Out, LIFO) 原则的特殊容器。

  • 操作:数据可以压入(Push) 栈顶,也可以从栈顶弹出(Pop)

  • 规则:最先压入的数据最后弹出,最后压入的数据最先弹出。(类比:叠放的盘子,总是取最上面的那个,最下面的盘子是最后才能被取到的)。

在计算机系统的具体实现中:

  • 栈是一块动态的内存区域

  • 压栈(Push) 使栈增大,出栈(Pop) 使栈减小。

  • 在经典的操作系统(如i386, x86-64架构)中,栈的增长方向是向下的,即从高地址低地址扩展。

  • 栈顶的位置由一个名为 esp (Stack Pointer) 的专用寄存器来定位和跟踪。

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

理解函数栈帧的操作需要了解一些关键的底层硬件寄存器和汇编指令。

a) 相关寄存器

寄存器全称与用途
eax通用寄存器,通常用于存储临时数据和函数的返回值
ebx通用寄存器,用于保留临时数据。
ebp栈底指针寄存器 (Base Pointer),用于定位当前函数栈帧的底部。在函数执行过程中,其值通常保持稳定,从而可以通过ebp方便地访问参数和局部变量。
esp栈顶指针寄存器 (Stack Pointer),始终指向系统栈的最顶部(下一个可用的最低地址)pushpop操作都会直接改变esp的值。
eip指令指针寄存器 (Instruction Pointer),保存着CPU下一条要执行的指令的地址。程序的执行流程就是由eip的指向决定的。

b) 相关汇编指令

汇编指令功能描述
mov数据转移指令。例如 mov eax, ebx 将ebx的值拷贝到eax中。
push数据入栈。1. 先将esp的值减小(栈向下增长)。2. 再将数据写入新的栈顶地址。
pop数据出栈。1. 先将esp指向的数据读出来。2. 再将esp的值增加(栈收缩)。
sub减法指令。常用于减小esp的值来为函数局部变量开辟空间。例如 sub esp, 0Ch
add加法指令。常用于增加esp的值来回收函数局部变量的空间。例如 add esp, 0Ch
call函数调用指令。它主要做两件事:
1. 压入返回地址:将call指令的下一条指令的地址压入栈中。
2. 转入目标函数:修改eip,开始执行被调用函数的代码。
jump跳转指令。通过直接修改eip寄存器的值,来改变程序的执行流程。
ret函数返回指令。它的作用类似于 pop eip,即call指令压入栈的返回地址弹出,并放入eip寄存器中,从而使程序跳回到调用者函数中继续执行。

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

1. 预备知识

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

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

如图所示:

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

2. 函数的调用堆栈

演示代码:

#include <stdio.h>int Add(int x, int y) 
{int z = 0;z = x + y;return z;
}int main() 
{int a = 3;int b = 5;int ret = 0;ret = Add(a, b);printf("%d\n", ret);return 0;
}

        这段代码,如果我们在VS2019编译器上调试,调试进入Add函数后,我们就可以观察到函数的调用堆栈 (右击勾选【显示外部代码】),如下图:

        函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main 函数调用之前,是由 invoke_main 函数来调用main函数。 在 invoke_main 函数之前的函数调用我们就暂时不考虑了。那我们可以确定, invoke_main 函数应该会有自己的栈帧, main 函数和 Add 函数也会维护自己的栈帧,每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间。 那接下来我们从main函数的栈帧创建开始讲解:

3. 准备环境

        为了让我们研究函数栈帧的过程足够清晰,不要太多干扰,我们可以关闭下面的选项,让汇编代码中排除一些编译器附加的代码,首先右击“解决方案”栏中的项目,打开如下,跟着步骤来:

4. 转到反汇编

        调试到main函数开始执行的第一行,右击鼠标找到“反汇编”选项并点击,转到反汇编。 注:VS编译器每次调试都会为程序重新分配内存,博客中的反汇编代码是一次调试代码过程中数据,每次调试略有差异。

int main()
{
//函数栈帧的创建
00BE1820  push        ebp  
00BE1821  mov         ebp,esp  
00BE1823  sub         esp,0E4h  
00BE1829  push        ebx  
00BE182A  push        esi  
00BE182B  push        edi  
00BE182C  lea         edi,[ebp-24h]  
00BE182F  mov         ecx,9  
00BE1834  mov         eax,0CCCCCCCCh  
00BE1839  rep stos    dword ptr es:[edi]  
//main函数中的核心代码int a = 3;
00BE183B  mov         dword ptr [ebp-8],3  int b = 5;
00BE1842  mov         dword ptr [ebp-14h],5  int ret = 0;
00BE1849  mov         dword ptr [ebp-20h],0  ret = Add(a, b);
00BE1850  mov         eax,dword ptr [ebp-14h]  
00BE1853  push        eax  
00BE1854  mov         ecx,dword ptr [ebp-8]  
00BE1857  push        ecx  
00BE1858  call        00BE10B4  
00BE185D  add         esp,8  
00BE1860  mov         dword ptr [ebp-20h],eax  printf("%d\n", ret);
00BE1863  mov         eax,dword ptr [ebp-20h]  
00BE1866  push        eax  
00BE1867  push        0BE7B30h  
00BE186C  call        00BE10D2  
00BE1871  add         esp,8  return 0;
00BE1874  xor         eax,eax  
}

5. 函数栈帧的创建

这里看到 main 函数转化来的汇编代码如上所示。 接下来我们就一行行拆解汇编代码

00BE1820  push        ebp    //把ebp寄存器中的值进行压栈,此时的ebp中存放的是invoke_main函数栈帧的ebp,esp-4
00BE1821  mov         ebp,esp  //move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp
00BE1823  sub         esp,0E4h  //sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据已经调试信息等。
00BE1829  push        ebx  //将寄存器ebx的值压栈,esp-4
00BE182A  push        esi  //将寄存器esi的值压栈,esp-4
00BE182B  push        edi  //将寄存器edi的值压栈,esp-4
//上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复。//下面的代码是在初始化main函数的栈帧空间。
//1. 先把ebp-24h的地址,放在edi中
//2. 把9放在ecx中
//3. 把0xCCCCCCCC放在eax中
//4. 将从edp-0x2h到ebp这一段的内存的每个字节都初始化为0xCC
00BE182C  lea         edi,[ebp-24h]  
00BE182F  mov         ecx,9  
00BE1834  mov         eax,0CCCCCCCCh  
00BE1839  rep stos    dword ptr es:[edi]

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

edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{*(int*)edi = eax;
}

小知识:烫烫烫~

        之所以上面的程序输出“烫”这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间的其中每一 个字节都被初始化为0xCC,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,0xCCCC(两 个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。接下来我们再分析main函数中的核心代码:

 int a = 3;
00BE183B  mov         dword ptr [ebp-8],3  //将3存储到ebp-8的地址处,ebp-8的位置其实就是a变量int b = 5;
00BE1842  mov         dword ptr [ebp-14h],5 //将5存储到ebp-14h的地址处,ebp-14h的位置其实是b变量int ret = 0;
00BE1849  mov         dword ptr [ebp-20h],0  //将0存储到ebp-20h的地址处,ebp-20h的位置其实是ret变量
//以上汇编代码表示的变量a,b,ret的创建和初始化,这就是局部的变量的创建和初始化
//其实是局部变量的创建时在局部变量所在函数的栈帧空间中创建的
//调用Add函数ret = Add(a, b);
//调用Add函数时的传参
//其实传参就是把参数push到栈帧空间中
00BE1850  mov         eax,dword ptr [ebp-14h]  //传递b,将ebp-14h处放的5放在eax寄存器中
00BE1853  push        eax                      //将eax的值压栈,esp-4
00BE1854  mov         ecx,dword ptr [ebp-8]    //传递a,将ebp-8处放的3放在ecx寄存器中
00BE1857  push        ecx                      //将ecx的值压栈,esp-4
//跳转调用函数
00BE1858  call        00BE10B4  
00BE185D  add         esp,8  
00BE1860  mov         dword ptr [ebp-20h],eax

Add函数的传参
//调用Add函数ret = Add(a, b);
//调用Add函数时的传参
//其实传参就是把参数push到栈帧空间中,这里就是函数传参
00BE1850  mov         eax,dword ptr [ebp-14h]  //传递b,将ebp-14h处放的5放在eax寄存器
中
00BE1853  push        eax                      //将eax的值压栈,esp-4
00BE1854  mov         ecx,dword ptr [ebp-8]    //传递a,将ebp-8处放的3放在ecx寄存器中
00BE1857  push        ecx                      //将ecx的值压栈,esp-4
//跳转调用函数
00BE1858  call        00BE10B4  
00BE185D  add         esp,8  
00BE1860  mov         dword ptr [ebp-20h],eax

函数调用过程
//跳转调用函数
00BE1858  call        00BE10B4  
00BE185D  add         esp,8  
00BE1860  mov         dword ptr [ebp-20h],eax

        call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

当我们跳转到Add函数,就要开始观察Add函数的反汇编代码了。

int Add(int x, int y)
{
00BE1760  push        ebp  //将main函数栈帧的ebp保存,esp-4
00BE1761  mov         ebp,esp   //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
00BE1763  sub         esp,0CCh  //给esp-0xCC,求出Add函数的esp
00BE1769  push        ebx       //将ebx的值压栈,esp-4
00BE176A  push        esi       //将esi的值压栈,esp-4
00BE176B  push        edi       //将edi的值压栈,esp-4int z = 0;      
00BE176C  mov         dword ptr [ebp-8],0  //将0放在ebp-8的地址处,其实就是创建zz = x + y;//接下来计算的是x+y,结果保存到z中
00BE1773  mov         eax,dword ptr [ebp+8]   //将ebp+8地址处的数字存储到eax中
00BE1776  add         eax,dword ptr [ebp+0Ch]  //将ebp+12地址处的数字加到eax寄存中
00BE1779  mov         dword ptr [ebp-8],eax    //将eax的结果保存到ebp-8的地址处,其实就是放到z中return z;
00BE177C  mov         eax,dword ptr [ebp-8]    //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。
}
00BE177F  pop         edi  
00BE1780  pop         esi  
00BE1781  pop         ebx  
00BE1782  mov         esp,ebp  
00BE1784  pop         ebp  
00BE1785  ret  

代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。

在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。

  • 将main函数的 ebp 压栈
  • 计算新的 ebp 和 esp
  • 将 ebx , esi , edi 寄存器的值保存
  • 计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的参数,这就是形参访问。
  • 将求出的和放在 eax 寄存器准备带回

        图片中的 a' 和 b' 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程,以及函数在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。

6. 函数栈帧的销毁

当函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。 那具体是怎么销毁的呢?我们看一下反汇编代码。

00BE177F  pop         edi  //在栈顶弹出一个值,存放到edi中,esp+4
00BE1780  pop         esi  //在栈顶弹出一个值,存放到esi中,esp+4
00BE1781  pop         ebx  //在栈顶弹出一个值,存放到ebx中,esp+4
00BE1782  mov         esp,ebp  //再将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈帧空间
00BE1784  pop         ebp  //弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈底。
00BE1785  ret              //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行。

回到了call指令的下一条指令的地方:

但调用完Add函数,回到main函数的时候,继续往下执行,可以看到:

00BE185D  add         esp,8                  //esp直接+8,相当于跳过了main函数中压栈的 'a'和b'
00BE1860  mov         dword ptr [ebp-20h],eax  //将eax中值,存档到ebp-0x20的地址处,其实就是存储到main函数中ret变量中,而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的。
拓展了解

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

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

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

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

相关文章

广告牌安全监测系统综合解决方案

一、方案背景 广告牌作为城市户外广告的重要载体&#xff0c;广泛分布于城市道路、商业区及交通枢纽等人流密集区域。由于长期暴露在自然环境中&#xff0c;广告牌面临着风荷载、雨雪侵蚀、温度变化等多重因素的影响&#xff0c;其结构安全性和稳定性直接关系到公共安全。近年来…

MII的原理

一、介绍 MII 是 Media Independent Interface&#xff08;媒体独立接口&#xff09; 的缩写&#xff0c;是一种用于连接网络物理层&#xff08;PHY&#xff09;芯片和数据链路层&#xff08;MAC&#xff09;芯片的标准硬件接口&#xff0c;核心作用是让不同类型的物理层&…

【Excel】Excel的工作场景

一、Excel的发展历史 1.1 版本迭代周期 自Excel 2019版本起&#xff0c;微软将更新周期稳定在每3年一次&#xff0c;而3年的周期刚好平衡了创新与稳定&#xff1a;既能紧跟大数据时代下用户对自动化、智能化处理的需求&#xff08;比如近年数据量激增带来的批量处理需求&#x…

nestjs 连接redis

1、下载npm install --save nestjs-modules/ioredis ioredis2、全局模块中引用RedisModule.forRootAsync({useFactory: (configService: ConfigService) > {return {type:"single",url: configService.get(redis.url) };},inject: [ConfigService], }),整体如下&…

需求管理需要哪些角色配合

需求管理是项目管理的关键组成部分&#xff0c;它确保项目目标得到准确理解并能顺利实现。有效的需求管理需要多个角色的紧密配合&#xff0c;包括项目经理、产品经理、需求分析师、开发人员、测试人员等。这些角色共同协作&#xff0c;确保需求从收集、分析、实施到验证的每一…

SqlHelper类的方法详细解读和使用示例

在 C# 数据库编程中&#xff0c;SqlHelper类是简化 SQL Server 操作的重要工具&#xff0c;它封装了ADO.NET的底层细节&#xff0c;让开发者能更专注于业务逻辑。以下从方法原理、使用示例和实战技巧三个方面进行详细说明。 一、SqlHelper 核心方法原理与对比 SqlHelper的方法…

智能一卡通系统通过集成身份识别、权限管理、数据联动等技术,实现多场景一体化管理。以下是多奥基于最新技术趋势和应用案例的系统解析

智能一卡&#xff08;码、脸&#xff09;通系统包括消费系统、梯控、门禁、停车场管理、访客机等&#xff0c;需要了解这些系统的集成应用和最新技术发展。多奥打算从以下几个维度来讲解。智能一卡通系统的整体架构和主要功能模块各子系统(门禁、梯控、停车场、访客管理等)的技…

嵌入式学习日志————USART串口协议

1.通信接口通信的目的&#xff1a;将一个设备的数据传送到另一个设备&#xff0c;扩展硬件系统通信协议&#xff1a;制定通信的规则&#xff0c;通信双方按照协议规则进行数据收发名称引脚双工时钟电平设备USARTTX&#xff08;数据发送脚&#xff09;、RX&#xff08;数据接收脚…

微软硬件笔试面试核心题型详细解析

微软硬件笔试面试核心题型详细解析 本专栏预计更新90期左右。当前第42期-笔试面试核心题型详细解析. 本文一共4个章节,核心内容如下。 微软作为全球顶尖的科技公司,其硬件工程师的选拔标准极高。笔试不仅考察扎实的理论基础,更注重解决实际问题的能力、对新技术的理解以…

CMake构建学习笔记21-通用的CMake构建脚本

在之前使用CMake构建程序的脚本&#xff08;CMake构建学习笔记-目录&#xff09;中&#xff0c;大部分内容都有比较强的相似性&#xff0c;那么是不是可以这些相似的内容提取出来作为一个单独的脚本&#xff0c;在构建具体的程序的时候再去调用这个脚本呢&#xff1f;这样做的好…

无人机和无人系统的计算机视觉-人工智能无人机

无人机和无人系统的计算机视觉-人工智能无人机将计算机视觉与无人系统相结合&#xff0c;可以提升其自主或半自主执行复杂任务的能力。这些系统将图像数据与其他机载传感器&#xff08;例如 GNSS/GPS、IMU、LiDAR 和热像仪&#xff09;融合&#xff0c;以解读周围环境并执行精确…

【开题答辩全过程】以 基于hadoop架构的教学过程监控系统为例,包含答辩的问题和答案

个人简介&#xff1a;一名14年经验的资深毕设内行人&#xff0c;语言擅长Java、php、微信小程序、Python、Golang、安卓Android等开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧…

坎坷基金路

2025年8月27日上午10:59从基金委官网上中外合作入口查到自己的基金中了。心情顿时五味杂陈&#xff0c;回想起写基金忙碌的9个月&#xff0c;各位专家对我的指导&#xff0c;嘴角楠楠的说&#xff1a;感恩。自己觉着比较重要的几个点&#xff1a;1、立意必须基于自己的过往研究…

[n8n]

docs&#xff1a;n8n工作流管理系统 本项目帮助管理和探索n8n工作流。 它能自动扫描和分析工作流文件&#xff0c;提取关键信息如名称、触发器和关联服务。 所有数据将存入可搜索的数据库&#xff0c;并通过REST API提供访问。 可以快速定位特定工作流、查看详细描述&#xff0…

[手写系列]Go手写db — — 第二版

[手写系列]Go手写db — — 第二版 第一版文章&#xff1a;[手写系列]Go手写db — — 完整教程 整体项目Github地址&#xff1a;https://github.com/ziyifast/ZiyiDB请大家多多支持&#xff0c;也欢迎大家star⭐️和共同维护这个项目~ 本文主要介绍如何在 ZiyiDB 第一版的基础上…

私有化大模型基础知识

私有化大模型基础知识 文章目录私有化大模型基础知识0x01.开源闭源2. 数据成本&#xff1a;昂贵且隐形的开销3. 研发投入&#xff1a;人力与时间成本总结&#xff1a;总成本量化更重要的是&#xff1a;持续投入和机会成本0x02.模型大小0x03.模型参数0x04.CPU和GPU0x05.GPU和模型…

Django时区处理

Django 的时区处理机制是为了确保在全球部署应用时&#xff0c;时间数据始终一致、可控&#xff0c;并能根据用户或系统需求灵活转换。下面我来系统地拆解一下 Django 的时区处理方式&#xff0c;帮你掌握从配置到实际应用的全过程。&#x1f9ed; 1. 基础配置&#xff1a;USE_…

SqlHelper类库的使用方法

使用 SqlHelper.dll 时&#xff0c;首先需要在项目中引用该 DLL&#xff0c;然后通过其封装的方法简化 SQL Server 数据库操作。以下是常见操作的 C# 示例代码&#xff1a;查看SqlHelper.dll方法内容// 替换为实际的SqlHelper.dll路径 using System.Reflection; using Microsof…

苍穹外卖项目实战(日记十一)-记录实战教程及问题的解决方法-(day3-3)完善菜品分页查询功能

菜品分页查询 &#xff08;1&#xff09;需求分析 &#xff08;2&#xff09;代码开发分析 DTO 前端给后端 &#xff0c;VO 后端给前端&#xff1b; vo是进行页面展示&#xff0c;dto是前后端数据交互的&#xff0c;pojo是对应数据库表字段 &#xff08;3&#xff09;DishCo…

C++ 力扣 704.二分查找 基础二分查找 题解 每日一题

文章目录二分查找&#xff1a;从基础原理到代码实现二分查找的特点&#xff1a;细节是坑&#xff0c;学会是宝算法重点&#xff1a;原理不只是“有序”&#xff0c;模板要懂不要背题目描述&#xff1a;LeetCode 704. 二分查找为什么这道题值得弄懂&#xff1f;为什么可以用二分…