linux链接、目标文件全解析

内容目录

内容目录

链接

  • 1. 静态链接
  • 2. 目标文件
  • 3. 可重定位目标文件
  • 4. 符号和符号表
  • 5. 符号解析
    • 5.1 链接器如何解析多重定义的符号
    • 5.2 与静态库链接
    • 5.3 链接器如何使用静态库来解析引用
  • 6. 重定位
    • 6.1 重定位条目
      - 6.2 重定位符号引用
      • 6.2.1 重定位PC相对引用
      • 6.2.2 重定位绝对引用
  • 7. 可执行目标文件
  • 8. 加载可执行目标文件
  • 9. 动态链接共享库
  • 10. 运行时从应用程序中加载和链接共享库
  • 11. 位置无关代码
    • 11.1 PIC数据引用
    • 11.2 PIC函数调用
  • 12. 处理目标文件的工具

链接

链接的定义: 链接(linking)是将各种代码和数据片段收集并组合为一个单一文件的过程。链接既可被执行于编译时(静态链接),也可被执行于加载时(load time),还可以被执行于运行时(动态链接)。

​ ——《深入理解计算机系统》

1. 静态链接

定义: 静态链接以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。

链接
可重定位目标文件
可执行目标文件

链接器主要完成两个任务:

  • **符号解析:**目标文件中定义和引用了符号,每个符号对应于一个函数、全局变量或者静态变量。符号解析就是将符号引用和符号定义关联起来。
  • **重定位:**编译器和汇编器总是生成从地址0开始的代码和数据,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使它们指向这个内存位置。

2. 目标文件

定义: 目标文件是编译器生成的中间文件,包含编译后的机器代码和数据,还没有被链接为最终的可执行文件。

目标文件有三种形式:

  • 可重定位目标文件。不能直接运行,必须与其它可重定位目标文件合并起来才能执行。
  • 可执行目标文件。可以被直接复制到内存中运行。
  • 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
静态链接
加载
动态链接
可重定位目标文件
可执行目标文件
运行
共享目标文件

目标文件地格式:

  • Linux: ELF格式 (Executable and Linkable File)
  • Windows: PE格式(Portable Executable)
  • MAC OS-X: Mach-O格式

3. 可重定位目标文件

image-20241012213836939
  • ELF头: 包含生成该目标文件的系统的字的大小和字节序,以及ELF头大小、目标文件类型、机器类型(如x86-64)、节头部表地文件偏移、节头部表中条目地数量和大小。夹在ELF头和节头部表之间的都是节:
  • .text: 已编译程序的机器代码。
  • .rodata: 只读数据。例如字符串字面值。
  • .data:已初始化的全局和静态变量。
  • .bss: 未初始化的全局和静态变量,以及所有被初始化为0的全局或者静态变量。该节的存在主要是为了节省磁盘空间 ,提高加载速度,因为未初始化的变量只需要描述其类型和大小即可,不需要预留存储空间。
  • .symtab: 符号表,存放在程序中定义和引用的函数全局变量信息。
  • .rel.text: 存访.text节中引用或定义的符号的重定位信息。对外部函数全局变量的调用的位置都需要进行修改,对本地函数的调用则不需要修改位置。
  • .rel.data: 存放.data节中引用或定义的所有全局变量的重定位信息。
  • .debug: 调试符号表,里面保存了程序中定义的局部变量和类型定义,程序中定义和引用的全局变量以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才会生成这张表。
  • .line: 原始C源文件中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会生成这张表。
  • .strtab: 一个字符串表,其内容包括.systab和.debug节中的符号表,以及节头部中的节名字。

4. 符号和符号表

每个可重定位目标模块都有一个符号表,其中有三种不同的符号:

  • 在本目标文件中定义并能够被其它模块引用的全局符号。
  • 有其它模块定义,并在本目标文件中引用的全局符号。
  • 只被本目标文件定义和引用的局部符号,即局部静态变量。
image-20241012233138820

上图是符号表中条目的结构。其中需要着重解释两个字段:

  1. section: 表示该符号所在的节,其值为其在节头部表中的索引。除了[3]中图中显示的节外,还有三个特殊的伪节,在节头部表中没有条目,分别是ABS(不该被重定位的符号),UNDEF(被引用但并未被定义的符号),COMMON(还未被分配位置的未初始化的数据条目)。
  2. value: 表示该符号在所在节中的偏移。

5. 符号解析

定义:链接器解析符号引用的方法是将每个符号引用与输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。

对于本地局部的符号(static函数、static全局变量、局部静态变量),编译器会确保它们有且只有一个定义。但是对于全局符号,编译器无法确定它们在别的目标文件中是否有定义,这个工作就交给了链接器。

当链接器无法找到一个符号的定义时,会直接抛出错误信息,如"undefined reference xxx"。那当链接器找到了一个符号的多个定义时,会发生什么呢?

5.1 链接器如何解析多重定义的符号

在编译时,编译器向汇编器输出每个全局符号,要么是强,要么是弱,汇编器会把强弱信息隐含在符号表中。所谓强符号,是指函数和已初始化的全局符号,而弱符号就是未初始化的全局变量。请牢记定义。

根据强弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:

  • 规则1:不允许有多个同名的强符号。
  • 规则2:如果有一个强符号和多个弱符号同名,那么选择强符号。
  • 规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。

例子:

// foo1.c
#include <stdio.h>

int x = 12;     // 强符号
int y = 12;     // 强符号

void f(void);
int main()
{
        f();
        printf("x: 0llx%x, y: 0llx%x\n", (long long)&x, (long long)&y);
        printf("x = 0x%x, 0x%x\n", x, y);
        return 0;
}

// foo2.c
double x;	// 弱符号
void f()
{
	x = 0.0;
}

上面的代码会引起什么错误?

显然,x这个符号存在多个定义,一强一弱的情况下链接器会使用强符号,但是在main函数中,由于首先调用了f函数,执行"x = 0.0",而在foo.c中x是double类型,占64位,而链接后x实际是int类型,占32位。当x和y位置相邻且y在x的后面时,x和y的值就都会被改变,引起非常难以定位的错误。

幸运的是,当我们怀疑有此类错误时,可以用GCC的 -fno-common标志这样的选项调用链接器,这样在遇到多重定义时会触发错误,或者-Werror,它会把所有警告都变为错误。

5.2 与静态库链接

静态库:将所有相关的目标模块打包位一个单独的文件,成为静态库(static library)。它可以用作链接器的输入。

在Linux系统中,静态库以一种称为存档(archive)的特殊文件格式存访在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。

linux平台GNU C编译器在编译时会自动链接libc.a这个C语言标准库。

使用ar工具创建静态库

> gcc -c addvec.c mulvec.c
> ar rcs libaddvec.a addvec.o mulvec.o

将main函数文件与库文件链接创建一个可执行文件:

> gcc -static -o prog2c main2.c -L. -laddrvec
# 或者
> gcc -static -o prog2c main2.c ./libaddrvec.a
image-20241013123751147
5.3 链接器如何使用静态库来解析引用

符合解析阶段,链接器从左到右按照在命令行中出现的顺序来扫描可重定位目标文件存档文件。在扫描过程中链接器维护着一个可重定位目标文件的集合E,一个未解析的符号的集合U, 一个在前面的输入文件中已经定义的符号集合D。初始时E、U、D均为空。

链接器处理扫描对象的规则如下:

  • 判断文件f是目标文件还是静态库文件(存档文件)
  • 如果f是目标文件,则将其加入E,同时修改U和D反应f中的符号定义和引用。
  • 如果f是归档文件,链接器会尝试匹配U中未解析的符号和由归档文件成员定义的符号,如果匹配成功,就将对应目标文件加入E,并修改U和D。最后未被加入的目标文件被简单丢弃。
  • 最后,链接器完成对输入文件的扫描后,如果U是非空的,链接器会输出错误并终止。否则它会合并和重定位E中的目标文件,构建输出的可执行文件。

根据这个规则可知:命令行中目标文件和静态库文件的顺序非常重要,如果定义一个符号的归档文件出现在引用这个符号的目标文件或归档文件之前,那么该引用就无法被解析

> gcc -static ./libvector.a main2.c
> /tmp/cc9XH6Rp.o: In function 'main': /tmp/cc9XH6Rp.o(.text+0xl8): undefined reference to 'addvec

6. 重定位

完成符号解析之后,链接器就知道了哪些目标文件会被加入到输出文件,以及每个符号对应的定义的位置,于是可以开始进行符号的重定位了。重定位就是为每个符号分配运行时地址。重定位由两步组成:

  • 重定位节和符号定义。在这一步,链接器将所有相同的节合并在一起,并且赋予它们运行时地址,然后赋予所有符号运行时地址。
  • 重定位节中的符号引用。在这一步,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标文件中的重定位条目。
6.1 重定位条目

编译器在生成目标代码时并不知道静态变量和函数最终会被放在什么位置,更不知道在外部定义的符号的最终位置,因此编译器每遇到一个对最终位置未知的目标引用,都需要生成一个重定位条目,来告诉链接器将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中,数据的重定位条目放在.rel.data中。

一起看下上图的重定位条目的结构:

  • offset: 需要被修改的引用的节内偏移。
  • type: 重定位类型,告知链接器如何修改引用。
  • symbol: 指向被引用的符号,在符号表中的索引。
  • addend: 一些类型的重定位需要使用它对被修改的引用值做偏移调整。

ELF定义了32中不同的重定位类型,我们只关心最基本的两种:

  • R_X86_64_PC32: 重定位一个使用32位PC(程序计数器)相对地址的引用。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC(程序计数器)的当前运行时值,得到目标地址。
  • R_X86_64_32, 重定位一个使用32位绝对地址的引用。

当程序规模较大,例如数据段与代码段的偏移超过32地址范围,那么就需要在编译时设置更大的代码模型

-mcmodle medium|large
6.2 重定位符号引用

看一下链接器重定位算法的伪代码:

其中refptr表示引用在节内的位置,ADDR(s)表示节s的运行时地址,ADDR(r.symbol)表示重定位条目r表示的符号的运行时地址。

6.2.1 重定位PC相对引用

从第7,8行可以看到,在重定位相对地址的引用时,链接器需要首先算出引用本身的运行时地址refaddr, 然后用符号定义的运行时地址ADDR(r.symbol),加上偏移调整,然后减去refaddr

6.2.2 重定位绝对引用

从第 13行可以看到,重定位绝对引用只需要,在符号定义的运行时地址上加上偏移调整即可。

7. 可执行目标文件

下图是一个典型的ELF可执行目标文件的结构:

image-20241013165649651

可以看到,它于ELF格式的可重定位目标文件结构类似又有所不同。其中.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。因为可执行文件时完全链接的,所以不再需要.rel节。

段(segment)与节(section)的关系

节section是编译时的概念,编译器将代码、数据等划分为不同的节。

段segment是执行时的概念,操作系统的加载器会根据段头部表将文件中的多个节合并到同一个段中加载到内存中。

可执行文件的连续的的片/段(chunk)会被映射到连续的内存区域。程序头部表描述了这种映射关系:

image-20241013194712551

8. 加载可执行目标文件

当在shell中运行可执行目标文件时:

> ./prog

shell进程会通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它。加载器将可执行目标文件中的代码和数据从磁盘复制到内存,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载

每个linux程序都有一个运行时内存映像,请看下图:

image-20241015130730474

实际上,由于虚拟内存和内存映射的存在,加载器并不是简单的把程序的各个段复制到内存中。实际上,除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射到虚拟页时才会进行复制。

9. 动态链接共享库

定义共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。

创建一个共享库:

> gcc -shared -fpic -o 	libvector.so addvec.c multvec.c

链接一个共享库

# 在加载时链接共享库
> gcc -o prog21 main2.c ./libvector.so

这里libvector.so会在加载时进行链接。

当加载器加载prog21这个可执行目标文件时,它注意到里面包含一个**.interp节,这一节包含动态链接器**的路径名,动态链接器本身就是一个共享目标,然后加载器会加载和运行这个动态链接器。动态链接器通过执行下面的重定位完成链接任务:

  • 重定位libc.so的文本和数据到某个内存段。
  • 重定位libvector.so的文本和数据到某个内存段。
  • 重定位prog21中所有对libc.so和libvector.so定义的符号的引用。

最后,动态链接器将控制转交给应用程序。

10. 运行时从应用程序中加载和链接共享库

linux系统为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库。

#include <dlfcn.h>
// 返回值:若成功则为指向句柄的指针,若失败则为NULL
void *dlopen(const char *filename, int flag);

// dlsym用来获取一个符号的地址
// 如果该符号不存在,就返回NULL
void *dlsym(void *handle, char *symbol);

// 如果没有其他共享库还在使用这个共享库,dlclose函数就卸载该共享库
void dlclose(void *handle);

// 返回前面的函数最近出现的错误,如果没有错误发生,就返回NULL
const char *dlerror(void);

下面给出一个使用这些函数在运行时连接一个共享库的例子:

// dll.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfnc.h>

int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];

int main()
{
    void *handle;
    void (*addvec)(int *, int *, int *, int);
    char *error;
    
    handle = dlopen("./libvector.so", RTLD_LAZY);
    if (!handle)
    {
        fprintf(stderr, "%s\n", dlerror());
        exit(1);
    }
    
    addvec = dlsym(handle, "addvec");
    if ((error = dlerror()) != NULL)
    {
        fprintf(stderr, "%s\n", error);
        exit(1);
    }
    
    addvec(x, y, z, 2);
    
    if (dlclose(handle) < 0)
    {
        fprintf(stderr, "%s\n", dlerror());
    	exit(1);
    }
    return 0;
}

编译这个程序

gcc -rdynamic -o prog2r dll.c -ldl	# 链接dl库

-rdynamic参数用于控制在生成可执行文件时,再动态符号表中保留全局符号,而非只保留需要在外部解析的符号。默认情况下,可执行文件不保存静态符号表。

11. 位置无关代码

定义:可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code, PIC)。共享库的编译必须总是使用该选项。

共享库的一个主要目的是允许多个进程共享内存中相同的代码,从而节省内存资源。那么这是如何实现的呢?

11.1 PIC数据引用

编译器在数据段开始的地方创建了一个表,叫做全局偏移量(Global Offset Table, GOT)。在GOT中,每个被这个目标模块引用的全局符号都有一个8字节的条目。编译器还为GOT中每个条目生成一个重定位记录。每个引用全局目标的目标模块都有自己的GOT。

image-20241015183418147

这里的关键是,最GOT[3]的PC相对引用中的偏移量是一个常数:0x2008b9。这是因为这样一个关键的事实:无论我们在内存中的何处加载一个目标模块(包括共享目标模块 ),数据段与代码段的距离总是保持不变。

这里指的是虚拟地址的上的距离,而不是物理内存上的距离,后者实际上是随机的。

如果addcnt是由 libvector.so 模块定义的 ,编译器可以利用代码段和数据段之间不变的距离,产生对addcnt的直接PC相对引用,并增加一个重定位,让链接器在构造这个共享模块时解析它。如果addcnt是由另一个共享模块定义的,那么就需要通过GOT进行间接访问。

11.2 PIC函数调用

编译器没有办法预测共享库中定义的函数的运行时地址,那么就需要在运行时进行某种重定位。一种做法是为该引用生成一条重定位记录,然后动态链接器在程序加载时解析它。但是,这种方法并不是PIC,因为它需要链接器修改调用模块的代码段。GNU编译器系统使用延迟绑定来解决这个问题。

延迟绑定通过两个数据结构来实现,分别是GOT和过程链接表(Procedure Linkage Table, PLT)。如果一个模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。GOT是数据段的一部分,而PLT是代码段的一部分。

linux的动态链接器默认采用延迟绑定机制,就是在函数首次被使用时才进行解析。但是它仍会在启动时就尝试加载所有共享库,以确保在需要时能够找到这些库并进行符号解析。

那么让我们看一下这两个表的内容:

  • 过程链接表(PLT):PLT是一个数组,其中每个条目是16字节。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。
  • 全局偏移量表(GOT):和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余每个条目对应一个被调用的函数,其地址需要在运行时被解析。初始时,每个GOT条目都指向对应PLT条目的第二条指令。
image-20241015191714102

上图展示了,GOT和PLT如何协同完成工作:

第一次调用addvec:

  1. 不直接调用addvec,程序进入PLT[2]。
  2. 间接跳转到GOT[4]所指的地址,即PLT[2]的下一条指令。
  3. 把addvec的ID(0x1)压入栈,跳转到PLT[0]。
  4. PLT[0]把GOT[1]压入栈(即把动态链接器的一个参数压入栈),然后跳转到GOT[2]。动态链接利用两个参数确定addvec的运行时位置,用这个地址重写GOT[4],再把控制传递给addvec。

后续调用addvec:

  1. 程序进入PLT[2]
  2. 此时GOT[4]已被填入addvec的运行时地址,因此直接执行addvec

12. 处理目标文件的工具

  • ar:创建静态库,插入、删除、列出和提取成员

  • strip: 从目标文件中删除符号表信息

  • nm: 列出一个目标文件的符号表中定义的符号

  • size: 列出目标文件中节的名字和大小

  • readelf: 显示一个目标文件的完整结构

  • objdump: 所有二进制工具之母。能够显示一个二进制文件的所有信息。

  • LDD: 列出一个可执行文件在运行时所需要的共享库。

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

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

相关文章

计算机系统的层次

目录 计算机系统的层次ISA&#xff08;指令集体系结构&#xff09; 计算机系统的层次 计算机硬件是基础指令集体系结构&#xff1a;将硬件的功能封装从指令供软件使用操作系统&#xff1a;提供人机交互界面、提供服务功能的内核例程语言处理系统&#xff1a; 语言处理程序&…

群晖通过 Docker 安装 GitLab

Docker 配置容器步骤都是大同小异的&#xff0c;可以参考&#xff1a; 群晖通过 Docker 安装 Gitea-CSDN博客 1. 在 Docker 文件夹中创建 GitLab&#xff0c;并创建子文件夹 2. 设置权限 3. 打开 Docker 应用&#xff0c;并在注册表搜索 gitlab-ce 4. 选择 gitlab-ce 映像运行…

什么是不同类型的微服务测试?

大家好&#xff0c;我是锋哥。今天分享关于【什么是不同类型的微服务测试&#xff1f;】面试题&#xff1f;希望对大家有帮助&#xff1b; 什么是不同类型的微服务测试&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 微服务架构中的测试可以分为多种类…

多尺度建模:从理论到实践的深入探讨

#1024程序员节 | 征文# 引言 在现代科学与工程中&#xff0c;很多现象和过程在不同的空间和时间尺度上展现出复杂性。因此&#xff0c;能够有效地进行多尺度建模&#xff0c;已经成为了许多领域&#xff08;如物理、生物、工程、环境科学等&#xff09;研究的一个重要方向。本…

vue后台管理系统从0到1(5)

文章目录 vue后台管理系统从0到1&#xff08;5&#xff09;完善侧边栏修改bug渲染header导航栏 vue后台管理系统从0到1&#xff08;5&#xff09; 接上一期&#xff0c;我们需要完善我们的侧边狼 完善侧边栏 我们在 element 组件中可以看见&#xff0c;这一个侧边栏是符合我们…

【操作系统】06.进程控制

一、进程创建 1.1 认识fork函数 在linux中fork函数是非常重要的函数&#xff0c;它从已存在进程中创建一个新进程。新进程为子进程&#xff0c;而原进程为父进程。 进程调用fork&#xff0c;当控制转移到内核中的fork代码后&#xff0c;内核将 分配新的内存块和内核数据结构…

Aspose.PDF功能演示:使用 JavaScript 从 PDF 中提取文本

在数据提取、业务文档自动化和文本挖掘方面&#xff0c;使用 JavaScript 从PDF中提取文本非常有用。它允许开发人员自动执行从 PDF 收集信息的过程&#xff0c;从而显著提高处理大量文档的生产力和效率。在这篇博文中&#xff0c;我们将学习如何使用 JavaScript 从 PDF 中提取文…

人工智能的未来应用与发展前景

随着人工智能&#xff08;AI&#xff09;技术的快速进步&#xff0c;我们正亲历着它在各行各业中带来的巨大变革。无论是医疗、企业管理&#xff0c;还是日常生活&#xff0c;AI 技术都在改变着我们的工作和生活方式。那么&#xff0c;人工智能的应用前景究竟如何&#xff1f;它…

【消息队列】RabbitMQ实现消费者组机制

目录 1. RabbitMQ 的 发布订阅模式 2. GRPC 服务间的实体同步 2.1 生产者服务 2.2 消费者服务 3. 可靠性 3.1 生产者丢失消息 3.2 消费者丢失消息 3.3 RabbitMQ 中间件丢失消息 1. RabbitMQ 的 发布订阅模式 https://www.rabbitmq.com/tutorials/tutorial-three-go P 生…

winUI3 c++ 入门 2、 样式

目录 一、winUI3 基本概念及样式 1、边距 2、如何使用样式 1)、布局控件内定义样式 2)、APP.xmal定义全局样式 3)、单独的样式文件 3.1)、新增字典资源 xmal 3.2)、在里面设置样式 3.3)、引用样式 3、更多样式修改 1)、修改默认属性 2)、修改所有的默认颜色…

垃圾收集器与内存分配机制(一)

目录 一、为什么我们要去了解垃圾收集和内存分配 二、对象已死&#xff1f; 1. 引用计数算法 2. 可达性分析算法 3. 再谈引用 4. 生存还是死亡 5. 回收方法区 三、垃圾收集算法 1. 简介 2. 分代收集理论 2.1. 弱分代/强分代假说 2.2. 前面两代假说的缺陷 3. 标记-清…

智能去毛刺:2D视觉引导机器人如何重塑制造业未来

机器人技术已经深入到各个工业领域中&#xff0c;为制造业带来了前所未有的变革。其中&#xff0c;2D视觉引导机器人技术以其精准、高效的特点&#xff0c;在去毛刺工艺中发挥着越来越重要的作用。本文将为您介绍2D视觉引导机器人技术的基本原理及其在去毛刺工艺中的应用&#…

blender 理解 积木组合 动画制作 学习笔记

一、学习blender视频教程链接 案例2&#xff1a;积木组合_动画制作_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Bt4y1E7qn?vd_sourced0ea58f1127eed138a4ba5421c577eb1&p10&spm_id_from333.788.videopod.episodes 二、说明 之前已经学习了如何制作积木组…

20 Shell Script输入与输出

标出输入、标准输出、错误输出 一、程序的基本三个IO流 一&#xff09;文件描述符 ​ 任何程序在Linux系统中都有3个基本的文件描述符 ​ 比如: ​ cd/proc/$$/fd ​ 进入当前shell程序对于内核在文件系统的映射目录中: [rootlocalhost ~]# cd /proc/$$/fd [rootlocalhos…

Ubuntu22.04环境搭建MQTT服务器

官网&#xff1a; https://mosquitto.org 1.引入库 sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa2.升级安装工具 sudo apt-get update 3.安装 sudo apt-get install mosquitto 4.安装客户端 sudo apt-get install mosquitto-clients5.添加修改配置文件 进…

微信小程序上传图片添加水印

微信小程序使用wx.chooseMedia拍摄或从手机相册中选择图片并添加水印&#xff0c; 代码如下&#xff1a; // WXML代码&#xff1a;<canvas canvas-id"watermarkCanvas" style"width: {{canvasWidth}}px; height: {{canvasHeight}}px;"></canvas&…

【Linux】冯诺依曼体系结构 OS的概念

&#x1fa90;&#x1fa90;&#x1fa90;欢迎来到程序员餐厅&#x1f4ab;&#x1f4ab;&#x1f4ab; 主厨&#xff1a;邪王真眼 主厨的主页&#xff1a;Chef‘s blog 所属专栏&#xff1a;青果大战linux 总有光环在陨落&#xff0c;总有新星在闪烁 前言废话&#xff1a…

将java项目jar包打包成exe服务

1.结构展示 2.注意事项 前提: 环境准备:jdk8 和 .net支持 { 1.控制面板》程序和功能》启用和关闭windows功能》.net的勾选》2.jdk8自行百度安装环境3.其他项目必须的软件环境安装等&#xff08;数据库...&#xff09; }第一次准备: 1.将打包好的jar包放到premiumServices.exe…

销冠教你如何转化观望客户

在销售实践中&#xff0c;常会遇到这样的场景&#xff1a;客户对我们的提案表现出极大的兴趣&#xff0c;但在执行阶段却显得迟疑&#xff0c;频繁表示“还需观望&#xff0c;再考虑”。这种态度不仅拖慢了项目进度&#xff0c;甚至可能导致项目完全停滞&#xff0c;从而错失宝…

Spring Boot技术栈在论坛网站开发中的应用

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…