你一定要搞明白的C函数调用方式与栈原理

C语言与CPP编程 2021-05-08 00:00

写在前面的话

这绝对不是标题党。而是C/C++开发中你必须要掌握的基础知识,也是高级技术岗位面试中高频题。我真的真的真的希望无论是学生还是广大C/C++开发者,都该掌握此文中介绍的知识。


正文

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

一个典型的栈帧

                           图 1     


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

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

并且,foo有两个局部的int变量(4个字节)。在这个简化的场景中,main调用foo,而程序的控制仍在foo中。这里,main是调用者(caller),foo是被调用者(callee)。
ESP被foo使用来指示栈顶。EBP相当于一个“基准指针”。从main传递到foo的参数以及foo本身的局部变量都可以通过这个基准指针为参考,加上偏移量找到。
由于被调用者允许使用EAXECXEDX寄存器,所以如果调用者希望保存这些寄存器的值,就必须在调用子函数之前显式地把他们保存在栈中。另一方面,如果除了上面提到的几个寄存器,被调用者还想使用别的寄存器,比如EBXESIEDI,那么,被调用者就必须在栈中保存这些被额外使用的寄存器,并在调用返回前回复他们。也就是说,如果被调用者只使用约定的EAXECXEDX寄存器,他们由调用者负责保存并回复,但如果被调用这还额外使用了别的寄存器,则必须有他们自己保存并回复这些寄存器的值。
传递给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函数调用过程中,一个栈帧是如何建立及消除的。

函数调用前调用者的动作

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

首先,main把EAXECXEDX压栈。这是一个可选的步骤,只在这三个寄存器内容需要保留的时候执行此步骤。
接着,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的标记处开始。
图2展示了call指令完成后栈的内容。图2及后续图中的粗线指示了函数调用前栈顶的位置。我们将会看到,当整个函数调用过程结束后,栈顶又回到了这个位置。

                          图 2      

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

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

push ebp
mov ebp, esp

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

                                图 3

 

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

sub esp, 20

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

                            图 4

 

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

被调用者返回前的动作 

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

mov esp, ebp
pop ebp

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

     

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

leave
ret

调用者在返回后的动作 

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

add esp, 12

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

看个具体的实例:

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

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

先来看一个概貌:

 116:   int main(int argc, char* argv[])
217:   {
300401070   push        ebp
400401071   mov         ebp,esp
500401073   sub         esp,40h
600401076   push        ebx
700401077   push        esi
800401078   push        edi
900401079   lea         edi,[ebp-40h]
100040107C   mov         ecx,10h
1100401081   mov         eax,0CCCCCCCCh
1200401086   rep stos    dword ptr [edi]
1318:        printf
("%d",test(10,90));
1400401088   push        5Ah
150040108A   push        0Ah
160040108C   call        @ILT+0(test) (00401005)
1700401091   add         esp,8
1800401094   push        eax
1900401095   push        offset string "%d" (0042201c)
200040109A   call        printf (004010d0)
210040109F   add         esp,8
2219:        return 0;
23004010A2   xor         eax,eax
2420:   }

下面来解释一下,


开始进入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):

100401076   push        ebx
200401077   push        esi
300401078   push        edi    入栈
400401079   lea         edi,[ebp-40h]
50040107C   mov         ecx,10h
600401081   mov         eax,0CCCCCCCCh
700401086   rep stos    dword ptr [edi]      

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

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

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

00401005   jmp       test (00401020)  

即转向被调函数test:

 18:    long test(int a,int b)
29:    {
300401020   push        ebp
400401021   mov         ebp,esp          
500401023   sub         esp,40h
600401026   push        ebx
700401027   push        esi
800401028   push        edi
900401029   lea         edi,[ebp-40h]
100040102C   mov         ecx,10h
1100401031   mov         eax,0CCCCCCCCh
1200401036   rep stos    dword ptr [edi]       //这些和上面一样
1310:        a = a + 3
;                                    
1400401038   mov         eax,dword ptr [ebp+8]     //ebp=0x12FF248 [0x12FF30]即取到了参数10
150040103B   add         eax,3
160040103E   mov         dword ptr [ebp+8],eax
1711:        b = b + 5;
1800401041   mov         ecx,dword ptr [ebp+0Ch]
1900401044   add         ecx,5
2000401047   mov         dword ptr [ebp+0Ch],ecx
2112:        return a + b;
220040104A   mov         eax,dword ptr [ebp+8]
230040104D   add         eax,dword ptr [ebp+0Ch]  //最后的结果保存在eax, 结果得以返回
2413:   }
2500401050   pop         edi                
2600401051   pop         esi
2700401052   pop         ebx
2800401053   mov         esp,ebp     //esp指向0x12FF24, test函数的堆栈空间被放弃,从当前函数栈顶返回到栈底
2900401055   pop         ebp           //此时ebp=0x12FF80, 恢复现场  esp=0x12FF28
3000401056   ret                          ret负责栈顶0x12FF28之值00401091弹出到指令寄存器中,esp=0x12FF30

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

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



从test函数返回,执行:

00401091   add         esp,8      

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

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

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

1004010A4   pop         edi
2004010A5   pop         esi
3004010A6   pop         ebx
4004010A7   add         esp,40h    //为啥不用mov esp, ebp? 是为了下面的比较
5004010AA   cmp         ebp,esp   //比较,若不同则调用chkesp抛出异常
6004010AC   call        __chkesp (00401150)  
7004010B1   mov         esp,ebp  
8004010B3   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,因此:

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

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

100401798   mov         dword ptr [ebp-14h],1
20040179F   mov         dword ptr [ebp-10h],2
3004017A6   mov         dword ptr [ebp-0Ch],3
4004017AD   mov         dword ptr [ebp-8],4
5004017B4   mov         dword ptr [ebp-4],5

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

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

结果如何?

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

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

129:       int *ptrB = (int*)((int)szNum + 1);
2004017C1   lea         ecx,[ebp-13h]
3004017C4   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。

C语言与CPP编程 C语言/C++开发,C语言/C++基础知识,C语言/C++学习路线,C语言/C++进阶,数据结构;算法;python;计算机基础等
评论
  • 在过去十年中,自动驾驶和高级驾驶辅助系统(AD/ADAS)软件与硬件的快速发展对多传感器数据采集的设计需求提出了更高的要求。然而,目前仍缺乏能够高质量集成多传感器数据采集的解决方案。康谋ADTF正是应运而生,它提供了一个广受认可和广泛引用的软件框架,包含模块化的标准化应用程序和工具,旨在为ADAS功能的开发提供一站式体验。一、ADTF的关键之处!无论是奥迪、大众、宝马还是梅赛德斯-奔驰:他们都依赖我们不断发展的ADTF来开发智能驾驶辅助解决方案,直至实现自动驾驶的目标。从新功能的最初构思到批量生
    康谋 2025-01-09 10:04 58浏览
  • 在当前人工智能(AI)与物联网(IoT)的快速发展趋势下,各行各业的数字转型与自动化进程正以惊人的速度持续进行。如今企业在设计与营运技术系统时所面临的挑战不仅是技术本身,更包含硬件设施、第三方软件及配件等复杂的外部因素。然而这些系统往往讲究更精密的设计与高稳定性,哪怕是任何一个小小的问题,都可能对整体业务运作造成严重影响。 POS应用环境与客户需求以本次分享的客户个案为例,该客户是一家全球领先的信息技术服务与数字解决方案提供商,遭遇到一个由他们所开发的POS机(Point of Sal
    百佳泰测试实验室 2025-01-09 17:35 57浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 107浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2025-01-09 09:58 43浏览
  • 1月7日-10日,2025年国际消费电子产品展览会(CES 2025)盛大举行,广和通发布Fibocom AI Stack,赋智千行百业端侧应用。Fibocom AI Stack提供集高性能模组、AI工具链、高性能推理引擎、海量模型、支持与服务一体化的端侧AI解决方案,帮助智能设备快速实现AI能力商用。为适应不同端侧场景的应用,AI Stack具备海量端侧AI模型及行业端侧模型,基于不同等级算力的芯片平台或模组,Fibocom AI Stack可将TensorFlow、PyTorch、ONNX、
    物吾悟小通 2025-01-08 18:17 53浏览
  • HDMI 2.2 规格将至,开启视听新境界2025年1月6日,HDMI Forum, Inc. 宣布即将发布HDMI规范2.2版本。新HDMI规范为规模庞大的 HDMI 生态系统带来更多选择,为创建、分发和体验理想的终端用户效果提供更先进的解决方案。新技术为电视、电影和游戏工作室等内容制作商在当前和未来提供更高质量的选择,同时实现多种分发平台。96Gbps的更高带宽和新一代 HDMI 固定比率速率传输(Fixed Rate Link)技术为各种设备应用提供更优质的音频和视频。终端用户显示器能以最
    百佳泰测试实验室 2025-01-09 17:33 60浏览
  • 在智能网联汽车中,各种通信技术如2G/3G/4G/5G、GNSS(全球导航卫星系统)、V2X(车联网通信)等在行业内被广泛使用。这些技术让汽车能够实现紧急呼叫、在线娱乐、导航等多种功能。EMC测试就是为了确保在复杂电磁环境下,汽车的通信系统仍然可以正常工作,保护驾乘者的安全。参考《QCT-基于LTE-V2X直连通信的车载信息交互系统技术要求及试验方法-1》标准10.5电磁兼容试验方法,下面将会从整车功能层面为大家解读V2X整车电磁兼容试验的过程。测试过程揭秘1. 设备准备为了进行电磁兼容试验,技
    北汇信息 2025-01-09 11:24 67浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 93浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球中空长航时无人机产值达到9009百万美元,2024-2030年期间年复合增长率CAGR为8.0%。 环洋市场咨询机构出版了的【全球中空长航时无人机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球中空长航时无人机总体规模,包括产量、产值、消费量、主要生产地区、主要生产商及市场份额,同时分析中空长航时无人机市场主要驱动因素、阻碍因素、市场机遇、挑战、新产品发布等。报告从中空长航时
    GIRtina 2025-01-09 10:35 60浏览
  • 一个真正的质量工程师(QE)必须将一件产品设计的“意图”与系统的可制造性、可服务性以及资源在现实中实现设计和产品的能力结合起来。所以,可以说,这确实是一种工程学科。我们常开玩笑说,质量工程师是工程领域里的「侦探」、「警察」或「律师」,守护神是"墨菲”,信奉的哲学就是「墨菲定律」。(注:墨菲定律是一种启发性原则,常被表述为:任何可能出错的事情最终都会出错。)做质量工程师的,有时会不受欢迎,也会被忽视,甚至可能遭遇主动或被动的阻碍,而一旦出了问题,责任往往就落在质量工程师的头上。虽然质量工程师并不负
    优思学院 2025-01-09 11:48 82浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 84浏览
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 111浏览
  • 职场是人生的重要战场,既是谋生之地,也是实现个人价值的平台。然而,有些思维方式却会悄无声息地拖住你的后腿,让你原地踏步甚至退步。今天,我们就来聊聊职场中最忌讳的五种思维方式,看看自己有没有中招。1. 固步自封的思维在职场中,最可怕的事情莫过于自满于现状,拒绝学习和改变。世界在不断变化,行业的趋势、技术的革新都在要求我们与时俱进。如果你总觉得自己的方法最优,或者害怕尝试新事物,那就很容易被淘汰。与其等待机会找上门,不如主动出击,保持学习和探索的心态。加入优思学院,可以帮助你快速提升自己,与行业前沿
    优思学院 2025-01-09 15:48 51浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦