浅谈函数调用!

C语言与CPP编程 2022-08-17 08:56

导语 |  在任意一门编程语言中,函数调用基本上都是非常常见的操作;我们都知道,函数是由调用栈实现的,不同的函数调用会切换上下文;但是,你是否好奇,对于一个函数调用而言,其底层到底是如何实现的呢?本文讲解了函数调用的底层逻辑实现。


一、汇编概述


既然要讲解函数调用的底层逻辑实现,那么汇编语言我们是绕不过的。


因此,首先来复习一下汇编相关的知识。


我们都知道,计算机只能读懂二进制指令,而汇编就是一组特定的字符,汇编的每一条语句都直接对应CPU的二进制指令,比如:mov rax,rdx就是我们常见的汇编指令。


汇编语言就是通过一条条的 助记符+操作数实现的,并且汇编指令经过汇编器(assemble,例如Linux下的as)转变为实际的CPU二进制指令。



(一)一个简单的汇编例子


上面讲的有些空洞,来看一个实际的例子:


; 将寄存器rsp的值存储到寄存器rbp中mov rbp, rsp
; 将四个字节的4存储到地址为rbp-4的栈上mov DWORD PTR [rbp-4], 4
; 将rsp的值减去16sub rsp, 16


需要注意的是:汇编语言是和实际底层的CPU息息相关的;上面的汇编格式使用的便是Intel的语法格式


常见的汇编语言有两种截然不同的语法:


  • Intel格式:optcode destination,source,类似于语法int i=4

  • AT&T格式:optcode source,destination,直观理解为move from source to destination


若将上面的Intel汇编改写为AT&T汇编,则为:


movq %rsp, %rbp
movl $4, -4(%rbp)
subq $16, %rsp


可以看到,AT&T汇编的另外一个特点是:有前缀和后缀


比如:前缀%,$;后缀q,l等等。


这些前缀后缀有特殊的意思,后文会讲解,不同的格式侧重点不太一样。



(二)常用汇编指令


下面是一些非常常用的汇编指令,在后文中都会用到:




二、通用寄存器概述


对于汇编语言,仅仅了解其语法内容是远远不够的!


由于汇编语言和CPU是息息相关的,因此在硬件层面我们还需要关注CPU的通用寄存器。


在所有CPU体系架构中,每个寄存器通常都是有建议的使用方法的,而编译器也通常依照CPU架构的建议来使用这些寄存器,因而我们可以认为这些建议是强制性的


(一)8086架构(16bit)


让我们把视线首先转移到8086


下图展示了在8086 CPU中的各个寄存器:



主要包括下面几类寄存器


  • 通用寄存器:均可用来存放地址和数据。


  • 指针和变量寄存器:用来存放,某一段内地址偏移量,用来形成操作数地址,主要用来再堆栈操作或者变址操作中使用。


  • 段寄存器:由于存储器空间是分段的,所以这些段寄存器则是每个段的首地址。


  • 指令指针:IP用来存放将要执行的下一条指令再现在代码段的偏移量,将这个偏移量+段寄存器中存放的基地址,就找到了下一条指令的地址。


  • 标志位寄存器:用来存放计算结果的特征,这些标志位常常被用作接下来程序运行的条件。


  • 8086处理器内部有8个16位的通用寄存器,也就是CPU内部的数据单元,分别是:AX、BX、CX、DX、SP、BP、SI、DI。


这些寄存器的作用主要是:暂存计算机过程中的数据


另外,AX、BX、CX、DX这四个寄存器又可以分为两个8位的寄存器来使用,分别是AH、AL、BH、BL、CH、CL、DH、DL。


注意:其中H表示高位(high),L表示低位(low)的意思。


下面来看下控制单元:


IP寄存器就是指令指针寄存器(Instruction Pointer Register),指向代码段中下一条指令的位置;CPU会根据它不断地从内存的代码段中取出指令并加载到CPU的指令队列中,然后交给运算单元去执行CS、DS、SS、ES这四个寄存器都是16位寄存器,用来存储进程的地址空间信息。


比如:


  • CS是代码段寄存器(Code Segment Register),通过它可以找到代码在内存中的位置。


  • DS是数据段寄存器(Data Segment Register),通过它可以找到数据在内存中的位置。


  • SS是栈寄存器(Stack Register),栈是程序运行过程所需要的一种数据结构,主要用于记录函数调用的关系。


  • ES是一个附加段寄存器(Extra Segment Register),当发现段寄存器不够用的时候,你可以考虑使用ES段寄存器。


如何根据上述段寄存器找到所需的地址呢?


CS和DS中都存放着一个段的起始地址,代码段的偏移值存放在IP寄存器中,而数据段的偏移值放在通用寄存器中;由于8086架构中总线地址是20位的,而段寄存器和IP寄存器以及通用寄存器都是16位的,所以为了得到20位的地址,先将段寄存器中起始地址左移4位,然后再加上偏移量,就得到了20位的地址;也正是由于偏移量是16位的,所以每个段最大的大小是64K的。


另外,对于20位的地址总线来说,能访问到的内存大小最多也就只有2^20=1MB。


如果计算得到某个要访问的地址是1MB+X,那么最后访问的是地址X,因为地址线只能发送低20位的。


  • 关于标志位


8086CPU设置了一个:16位标志寄存器PSW(也叫FR),其中规定了9个标志位,用来存放运算结果特征和控制CPU操作。



9个标志位可以分为两类大类:


  • 条件码


  • 控制标志位


其中条件码包括:


  • OF(Overflow Flag)溢出标志,溢出时为1,否则置0:标明一个溢出了的计算,如:结构和目标不匹配。


  • SF(Sign Flag)符号标志,结果为负时置1,否则置0。


  • ZF(Zero Flag)零标志,运算结果为0时置1,否则置0。


  • CF(Carry Flag)进位标志,进位时置1,否则置0;注意:Carry标志中存放计算后最右的位;


  • AF(Auxiliary carry Flag)辅助进位标志,记录运算时第3位(半个字节)产生的进位置。有进位时1,否则置0;


  • PF(Parity Flag)奇偶标志,结果操作数中1的个数为偶数时置1,否则置0;


控制标志位包括:


  • DF(Direction Flag)方向标志,在串处理指令中控制信息的方向。


  • IF(Interrupt Flag)中断标志。


  • TF(Trap Flag)陷井标志。



(二)x86架构


接着,让我们步入32位机时代,来看看x86体系下的CPU寄存器:



可以看到,为了使得运行在8086架构上的程序在移到32位架构之后也能执行,32位架构对8086架构进行了兼容:


  • 通用寄存器从16位变成了32位,也就是8个32位的通用寄存器;但是为了保持兼容,仍然保留了16位和8位的使用方式,即:AH、AL等。


  • 指向下一条指令的指令指针寄存器也从16位变成了32位,被称为EIP,但是同样兼容16位的使用方式。


  • 段寄存器改动比较大:在32位架构中段寄存器还是16位,但是它不再表示段的起始地址,而是表示索引;32位架构中,引入了段描述符表,表格中的每一项都是段描述符(Segment Descriptor),记录了段在内存中的起始位置,而这张表则存放在内存的某个地址;那么,段寄存器中存的就是对应段在段表中的位置,称为选择子(selector)。


关于选择子


先根据段寄存器拿到段的起始地址,再根据段寄存器中保存的选择子,找到对应的段描述符,然后从这个段描述符中取出这个段的起始地址;就相当于由之前的直接找到段起始地址变成了间接找到段起始地址;这样改变之后,段起始地址会变得很灵活。


但是这样就跟原来的8086架构不兼容了,因此为了兼容8086架构,32位架构中引入了实模式和保存模式:8086架构中的方式就称为实模式,32位这种模式就被称为保护模式。


当系统刚刚启动的时候,CPU是处于实模式的,这个时候和8086模式是兼容的;当需要更多内存时,进行一系列的操作,将其切换到保护模式,这样就能使用32位了。


模式可以理解为:CPU和操作系统的一起干活的模式:


  • 在实模式下,两者约定好了这些寄存器是干这个的,总线是这样的,内存访问是这样的。


  • 在保护模式下,两者约定好了这些寄存器是干那个的,总线是那样的,内存访问是那样的。


这样操作系统给CPU下命令,CPU按照约定好的,就能得到操作系统预料的结果,操作系统也按照约定好的,将一些数据结构,例如段描述符表放在一个约定好的地方,这样CPU就能找到。两者就可以配合工作了。


下面是x86平台下一些寄存器的调用特殊约定:



作为通用寄存器,过程调用中,调用者栈帧需要寄存器暂存数据,被调用者栈帧也需要寄存器暂存数据


为防止调用过程中数据不会被破坏丢失,C/C++编译器遵守如下约定的规则:



当产生函数调用时,子函数内通常也会使用到通用寄存器,那么这些寄存器中之前保存的调用者(父函数)的值就会被覆盖!为了避免数据覆盖而导致从子函数返回时寄存器中的数据不可恢复,CPU 体系结构中就规定了通用寄存器的保存方式。


如果一个寄存器被标识为Caller Save, 那么在进行子函数调用前,就需要由调用者提前保存好这些寄存器的值,保存方法通常是把寄存器的值压入堆栈中,调用者保存完成后,在被调用者(子函数)中就可以随意覆盖这些寄存器的值了。


如果一个寄存被标识为Callee Save,那么在函数调用时,调用者就不必保存这些寄存器的值而直接进行子函数调用,进入子函数后,子函数在覆盖这些寄存器之前,需要先保存这些寄存器的值,即这些寄存器的值是由被调用者来保存和恢复的。


具体来讲


当该函数是处于调用者角色时,如果该函数执行过程中产生的临时数据会已存

储在%eax,%edx,%ecx这些寄存器中,那么在其执行call指令之前会将这些寄存器的数据写入其栈帧内指定的内存区域,这个过程叫做调用者保存约定(Caller Save)。


当该函数是处于被调用者角色时,那么在其使用这些寄存器%ebx,%esp,%edi之前,那么该函数会保存这些寄存器中的信息到其栈帧指定的内存区域,这个过程叫被调用者保存约定;%eax总会被用作返回整数值。


%esp,%ebp总被分别用着指向当前栈帧的顶部和底部,主要用于在当前函数推出时,将他们还原为原始值;往往会在栈帧开始处保存上一个栈帧的ebp,而esp是全栈的栈顶指针,一直指向栈的顶部。


注:在x86-64架构下也是类似的约定!



(三)x86-64架构


  • 寄存器约定


最后就是我们目前主流的x86-64架构了;


对于x86-64架构,最常用的有16个64位通用寄存器,各寄存器及用途如下所示:



从上面的表可以看到,除了扩展原来存在的通用寄存器,x64架构还引入了8个新的通用寄存器:r8-r15。


这些寄存器虽然都可以用,但是还是做了一些规定,如下:


函数返回值存放的寄存器:rax。


  • rax同时也用于乘法和除法指令中。在imul指令中,两个64位的乘法最多会产生128位的结果,需要rax与rdx共同存储乘法结果,在div指令中被除数是128位的,同样需要rax与rdx共同存储被除数。


  • rsp是堆栈指针寄存器,通常会指向栈顶位置,堆栈的pop和push操作就是通过改变rsp的值即移动堆栈指针的位置来实现的。


  • rbp是栈帧指针,用于标识当前栈帧的起始位置。


  • %rdi,%rsi,%rdx,%rcx,%r8,%r9六个寄存器用于存储函数调用时的6个参数(如果有6个或6个以上参数的话)。


  • rbx被标识为 “miscellaneous registers”,属于通用性更为广泛的寄存器,编译器或汇编程序可以根据需要存储任何数据。


  • rbx、rbp、r12、r13、r14、r15:这些寄存器由被调用者负责保护,在返回的时候要恢复这些寄存器中原本的值。


同时,和上面x32架构类似这里也要区分Caller Save和Callee Save寄存器,即寄存器的值是由 调用者保存 还是由 被调用者保存。



  • 函数传参优化


在x32的时代,通用寄存器少,参数传递都是通过入栈(汇编指令push)实现的(当然也有使用寄存器传递的,比如著名的C++ this指针使用ecx寄存器传递,不过能用的寄存器毕竟不多),相对CPU寄存器来说,访问太慢,函数调用的效率就不高;


而在x86-64时代,寄存器数量多了,CPU就可以利用额外的寄存器rdi、rsi、rdx、rcx、r8、r9来存储参数!


寄存器传参的好处是速度快,减少了对内存的读写次数


注:多于6个的参数,依然还是通过入栈实现传递。


因此在x86_64位机器上编程时,需要注意:


  • 为了效率尽量使用少于6个参数的函数。


  • 传递比较大的参数,尽量使用指针,因为寄存器只有64位。


注意:具体使用栈还是用寄存器传参数,这个不是编程语言决定的,而是编译器在编译生成CPU指令时决定的。如果编译器非要在x64架构CPU上使用线程栈来传参那也不是不行,这个对高级语言是无感知的。



(三)x86-64寄存器的向下兼容


上述的寄存器名字都是64位的名字,对于每个寄存器,我们还可以只使用它的一部分,并使用另一个新的名字:




下面这些寄存器可能也会需要用到其他寄存器:


  • 8个80位的x87寄存器(%st0~st7),用于浮点计算。


  • 8个64位的MMX寄存器,用于MMX指令(多媒体指令),这8个寄存器跟 x87寄存器在物理上是相同的寄存器。


  • 16个128位的SSE寄存器,用于SSE指令。


  • RIP指令寄存器,保存指令地址。


  • flags(rflags-64位,eflags-32位)寄存器。每个位用来标识一个状态。比如,这些标识符可能用于比较和跳转的指令。

和上面所述的x86架构类似,在x86-64架构下也存在实模式;更多关于 x86-64 处理器架构: 

http://c.biancheng.net/view/3460.html

https://www.cnblogs.com/mazhimazhi/p/15236954.html


更多关于CPU寄存器历史见:
https://zhuanlan.zhihu.com/p/272135463



三、函数调用结构


上文简单复习了一下汇编和寄存器相关的内容。下面来正式来看看函数调用的底层是如何实现的!


:这里的说明采用的是:


  • 编译器:GCC 12.1。


  • 优化级别为-O0。


  • 汇编指令为intel架构。


(一)函数调用


子函数调用时,调用者与被调用者的栈帧结构如下图所示:



在子函数调用时,需要切换上下文使得当前调用栈进入到一个新的执行中:


  • 父函数将调用参数从后向前压栈:由函数调用者完成(上文中的Caller逻辑)。


  • 将返回地址压栈保存:call指令完成。


  • 跳转到子函数起始地址执行:call指令完成。


  • 子函数将父函数栈帧起始地址(%rpb)压栈:由函数被调用者完成(上文中的Callee逻辑);


  • 将%rbp的值设置为当前%rsp的值,即将%rbp指向子函数栈帧的起始地址:由函数被调用者完成(上文中的Callee逻辑),完成函数上下文的切换。


保存返回地址和保存上一栈帧的%rbp都是为了函数返回时,恢复父函数的栈帧结构(保存函数调用上下文)。


在使用高级语言进行函数调用时,由编译器自动完成上述整个流程;甚至对于”Caller Save”和“Callee Save”寄存器的保存和恢复,也都是由编译器自动完成的。


需要注意的是:父函数中进行参数压栈时,顺序是从后向前进行的(调用栈空间都是从大地址向小地址延伸,这一点刚好和堆空间相反)。


这一行为并不是固定的,是依赖于编译器的具体实现的。


至少在GCC中,使用的是从后向前的压栈方式,这种方式便于支持类似于printf(“%d,%d”,i,j) 这样的使用变长参数的函数调用。


以下面的函数为例:


void func() {}
void my_func() { func();}


对应的汇编为:


func():        push    rbp        mov     rbp, rsp        nop        pop     rbp        retmy_func():        push    rbp        mov     rbp, rsp        call    func()        nop        pop     rbp        ret


在函数my_func和func中:开始的两句就是由编译器默认生成的切换上下文语句(函数my_func中也存在这个语句是因为它最终也会被其他函数s调用)。


当my-func函数调用func函数时:


  • 首先,执行call指令,保存返回地址,并跳转至func函数起始地址(这里没有压栈调用参数是因为func入参为空)。


  • 随后,在func函数中,使用push rbp和mov rbp,rsp保存上下文,随后开始执行func函数中的逻辑。


  • 由于没有代码,且没有返回值,此次为nop指令。


  • 最后,恢复上下文,并返回(函数返回在下文中介绍)。


函数开头的push rbp和mov rbp,rsp又叫做函数的序言(prologue),几乎每个函数一开始都会该指令。


它和函数最后的pop rbp和ret(epilogue)起到维护函数的调用栈的作用。


接下来,顺理成章的我们来看一下函数的返回过程。



(二)函数返回


函数返回时,我们只需要得到函数的返回值(保存在%rax中),之后就需要将栈的结构恢复到函数调用之差的状态,并跳转到父函数的返回地址处继续执行即可。


由于函数调用时已经保存了返回地址和父函数栈帧的起始地址,要恢复到子函数调用之前的父栈帧,我们只需要执行以下两条指令:


pop rbpret


首先执行pop rbp指令,直接将调用栈地址恢复至调用函数之前的状态。


随后通过ret指令跳转至返回地址处并执行。



(三)数据参数传递


  • 函数参数传递概述


在函数调用中,另一个需要关注的便是函数参数的传递:入参传递以及返回值传递。


函数在计算的时候,存储数据的地方总共有三个:


  • 寄存器;


  • 内存:栈空间、堆(heap)空间、静态区。


  • 程序本身:只读的程序数据片段,比如int i=4,这个4存储于程序本身,在汇编里面又叫立即数(immediate number)。


知道了数据的存储地方,那么数据的传递就分为以下四个方面:


  • 从内存到寄存器;


  • 从寄存器到内存;


  • 从立即数到寄存器;


  • 从立即数到内存。


注意:数据不能从内存直接传递到内存,如果需要从内存传递到内存,要以寄存器为中介!


同时需要注意的是:数据是有大小的!


比如:一个word是两个字节(16bit),double words是四个字节(32bit),quadruple words是八个字节(64bit)。


所以传递数据的时候,要知道传递的数据大小


Intel格式的汇编会在数据前面说明数据大小:比如 mov DWORD PTR [rbp-4],4,意思是将一个4字节的4存储到栈上(地址为rbp-4)。


而AT&T格式是通过指令的后缀来说明,同样的指令为movl $4, -4(%rbp);并且存储的地方,AT&T汇编是通过前缀来区别,比如%q前缀表示寄存器,$表示立即数,()表示内存。


学习了数据的传递方式之后,让我们看看函数的调用习惯。


  • 函数参数传递约定


之前我们简单学习了一下Caller和Callee的区别,在这里我们会深入的学习。


首先,什么是函数调用约定?


在Caller调用Callee时,要将参数(arguements)传递给Callee,一个函数可以接收多个参数,而Caller与Callee之间约定的每个参数的应该怎么传递就是调用习惯;这样,Callee才能到指定的位置获取到相应的参数。


比如下面的代码:


int square(int num) {    return num * num;}
int main() { int i = 4; int j = square(i);}


在main函数中调用square,参数i是如何传递到square中的?


上面的代码对应的汇编如下:


square(int):        push    rbp        mov     rbp, rsp        mov     DWORD PTR [rbp-4], edi        mov     eax, DWORD PTR [rbp-4]        imul    eax, eax        pop     rbp        retmain:        push    rbp        mov     rbp, rsp        sub     rsp, 16        mov     DWORD PTR [rbp-4], 4        mov     eax, DWORD PTR [rbp-4]        mov     edi, eax        call    square(int)        mov     DWORD PTR [rbp-8], eax        mov     eax, 0        leave        ret


通过上面的汇编,我们可以知道:


在main里面,4先存到栈上(mov DWORD PTR [rbp-4],4),然后存在edi里面(mov eax,DWORD PTR [rbp-4]、mov edi,eax),而sqaure函数直接就从edi里面读取4的值了!


这就说明:参数4是通过寄存器edi传给了callee (sqaure) 。


可能有同学会以为,从代码看,参数不是直接就传给了sqaure吗?实际上,在汇编中,这个变量i是不存在的,只有寄存器和内存,因此我们需要约定好入参i的值存在哪里。


下面让我们来详细看看这些约定、常见寄存器负责传递的参数以及一些作用(前文简要介绍了一些):



在上面的列表中:


  • 蓝色的是callee-owned、绿色背景的是caller-owned。


  • callee-owned表明:callee可以自由地使用这些寄存器,覆盖已有的值;如果caller要使用这些寄存机,那么它在调用callee前,要把这些寄存器保存好;例如:如果寄存器%rax的值caller想要保留,那么在调用函数之前,caller需要赋值这个值到“安全”的地方。


  • caller-owned表明:如果callee要使用这些寄存器,那么它就要保存好这些寄存器的值,并且返回到caller的时候要将这些值恢复;caller-owned的寄存器通常用于caller需要在函数之间保留的局部状态。


  • 一共有六个通用的寄存器用于传递参数;按顺序传递需要通用寄存器传递的参数,如果通用寄存器使用完了,那么就使用栈来传递。


同时,如果函数返回比较大的对象,那么第一个参数rdi会用来传递存储这个对象的地址(这个地址是由caller分配的)


有了这些基础,我们就更容易理解C++中的copy elision了。


相关阅读:


  • 深入理解C++中的move和forward


  • Copy/move elision: C++ 17 vs C++ 11



四、常见控制结构


在知道了函数参数是如何传递的之后,我们来更升一级。


下面根据具体代码来看一看我们经常使用的if、for、while等控制结构在底层是如何实现的。


if,while循环等控制结构,在汇编里面,都是基于判定语句,跳转语句实现的:做一个计算,检查相应的flag,然后根据flag的值确定要跳转到哪里


比如下面的if语句:


int multiply(int j) {    if (j > 6) {        return j*2;    } else {        return j*3;    }}


对应的汇编语句如下:


multiply(int):        push    rbp        mov     rbp, rsp        mov     DWORD PTR [rbp-4], edi        cmp     DWORD PTR [rbp-4], 6        jle     .L2        mov     eax, DWORD PTR [rbp-4]        add     eax, eax        jmp     .L3.L2:        mov     edx, DWORD PTR [rbp-4]        mov     eax, edx        add     eax, eax        add     eax, edx.L3:        pop     rbp        ret


最前面和最后两条命令就是函数调用中的上下文切换,这个在前文中已经详细说明了。


函数的逻辑从第三条语句真正开始:


mov DWORD PTR [rbp-4],edi表示将寄存器edi中的4个字节的值(DWORD PTR)移至 [rbp-4] 对应内存地址中。


这里和上面所讲述的参数传递的约定是保持一致的,因为我们的入参j是int类型,只有32位,因此使用的是edi寄存器来传递的参数。


随后,使用cmp指令将内存中的数和立即数6进行比较(即,j>6),此指令会改变标志寄存器%eflags的状态。


然后jle会利用标志寄存器%eflags中的状态进行跳转:


  • 如果j<=6,跳转至.L2。


  • 否则继续向下执行(对应j>6的场景)。


无论是向下执行还是跳转至.L2执行,最终两者都会执行至.L3并返回。


下面再来看一个for循环的例子:


int add(int j) {    int ret = 0;    for (int i = 0; i < j; ++i) {        ret+= i;    }    return ret;}


对应的汇编如下:


add(int):        push    rbp        mov     rbp, rsp        mov     DWORD PTR [rbp-20], edi        mov     DWORD PTR [rbp-4], 0        mov     DWORD PTR [rbp-8], 0        jmp     .L2.L3:        mov     eax, DWORD PTR [rbp-8]        add     DWORD PTR [rbp-4], eax        add     DWORD PTR [rbp-8], 1.L2:        mov     eax, DWORD PTR [rbp-8]        cmp     eax, DWORD PTR [rbp-20]        jl      .L3        mov     eax, DWORD PTR [rbp-4]        pop     rbp        ret


从上面的汇编我们可以看到,入参j依旧是由寄存器edi传递,并存储在了内存[rbp-20]中。


随后两行分别初始化了参数ret:[rbp-4]、i:[rbp-8]。


紧接着,指令直接跳转至.L2处,首先比较了[rbp-8]和[rbp-20]中的值(即比较i和j):如果i


这里的判断是符合for循环的逻辑的:在进入for循环之前首先会判断一次条件。


.L3代码块是for循环的真正逻辑:


; ret += i;mov     eax, DWORD PTR [rbp-8]add     DWORD PTR [rbp-4], eax
; ++iadd DWORD PTR [rbp-8], 1


其他控制结构的逻辑也是类似的,这里不再赘述了!


五、总结


本文首先简要复习了汇编以及通用寄存器相关的内容,随后进入到文章主题:函数调用


在函数调用中讲述了函数调用中的调用和返回细节、上下文切换保护、函数传递等内容。


最后略微引申了函数中常见控制结构的底层实现。


参考资料:

1.《程序是怎样跑起来的》

2.《程序员的自我修养 : 链接、装载与库》

3.https://zhuanlan.zhihu.com/p/368962727

4.https://zhuanlan.zhihu.com/p/27339191

5.http://c.biancheng.net/view/3460.html

6.https://zhuanlan.zhihu.com/p/288636064

7.https://zhuanlan.zhihu.com/p/272135463

8.http://119.23.219.145/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84-x86-%E6%9E%B6%E6%9E%84%E7%9A%84%E8%AE%B2%E8%A7%A3/

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