链接与加载-NJU-JYY

(2021) 19 [代码讲解] 从零实现动态加载

南京大学操作系统课蒋炎岩老师网络课程笔记。

视频:https://www.bilibili.com/video/BV1N741177F5?p=15
讲义:http://jyywiki.cn/OS/2021/slides/C9.slides#/

背景

回顾:

ELF可执行文件

只要能完成手册上要求的内容,就可以自己实现一个execve

本次课内容与目标

静态链接与加载

  • hello程序的链接与加载

动态链接与加载

  • 理解动态加载的动机
  • 自己实现动态加载
  • “lazy” 的延迟符号绑定
  • ELF文件的动态链接与加载

不同的ELF文件的格式

同为Linux中的可执行二进制文件ELF,也有不同的具体的格式。

我们拿一个hello程序举例:

#include <stdio>int main(){printf("Hello\n");
}

我们用不同的编译选项来编译它,注意gcc默认是动态链接:

gcc hello.c -o dynamic_hello
gcc -static hello.c -o static_hello

两个文件dynamic_hellostatic_hello都可以正常运行输出正常的结果,并且它们也都是ELF可执行文件。但是当我们用file命令来查看它们时,会发现有些许不同。

file dynamic_hello
file static_hello

输出分别为:

dynamic_hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=83a317b39ca06b8945b090871745ff8287463528, not stripped

static_hello: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=d9ac8fd547a3c93a5cafa2d47c2416b4a147180f, not stripped

注意上面红字标出的地方指明了这两个ELF文件之间的不同,它们一个是动态链接加载的,一个是静态链接加载的。

静态链接与加载

编译、链接的需求

为了节省空间和时间,不将所有的代码都写在同一个文件中是一个很基本的需求。

为此,我们的C语言需要实现这样的需求:允许引用其他文件(C标准成为编译单元,Compilation Unit)里定义的符号。C语言中不禁止你随便声明符号的类型,但是类型不匹配是Undefined Behavior。

假如我们有三个c文件,分别是a.cb.cmain.c

// a.c
int foo(int a, int b){return a + b;
}
// b.c
int x = 100, y = 200;
// main.c
extern int x, y;
int foo(int a, int b);
int main(){printf("%d + %d = %d\n", x, y, foo(x, y));
}

我们在main.c中声明了外部变量x,y和函数foo,C语言并不禁止我们这么做,并且在声明时,C也不会做什么类型检查。当然,在编译main.c的时候,我们看不到这些外部变量和函数的定义,也不知道它们在哪里。

我们编译链接这些代码,Makfile如下:

CFLAGS := -Osa.out: a.o b.o main.ogcc -static -Wl,--verbose a.o b.o main.oa.o: a.cgcc $(CFLAGS) -c a.cb.o: b.cgcc $(CFLAGS) -c b.cmain.o: main.cgcc $(CFLAGS) -c main.cclean:rm -f *.o a.out

结果生成的可执行文件可以正常地输出我们想要的内容。

make
./a.out
# 输出:
# 100 + 200 = 300

我们知道foo这个符号是一个函数名,在代码区。但这时,如果我们将main.c中的foo声明为一个整型,并且直接打印出这个整型,然后尝试对其加一。即我们将main.c改写为下面这样,会发生什么事呢?

// main.c (changed)
#include <stdio.h>
extern int x, y;
// int foo(int a, int b);
extern int foo;
int main(){printf("%x\n", foo);foo += 1;// printf("%d + %d = %d\n", x, y, foo(x, y));
}

输出:

c337048d
Segmentation fault (core dumped)

我们发现,其实是能够打印出四个字节(整型为4个字节),但这四个字节是什么东西呢?

C语言中的类型C语言中的其实是可以理解为没有类型的,在C语言的眼中只有内存和指针,也就是内存地址,而所谓的C语言中的类型,其实就是对这个地址的一个解读。比如有符号整型,就按照补码解读接下来的4个字节地址;又比如浮点型,就是按照IEEE754的浮点数规定来解读接下来的4字节地址。

那我们这里将符号foo定义为了整型,那编译器也会按照整型4个自己来解读它,而这个地址指针指向的其实还是函数foo的地址。那这四个字节应该就是函数foo在代码段的前四个字节。我们不妨用objdump反汇编来验证我们的想法:

objdump -d a.out

输出(节选):

在这里插入图片描述

我们看到,foo函数在代码段的前四个字节的地址确是就是我们上面打印输出的c3 37 04 8d(注意字节序为小端法)。

那我们接下来试图对foo进行加一操作相当于是对代码段的写操作,而我们知道内存中的代码段是 可读可执行不可写 的,这就对应了上面输出的Segmentation fault (core dumped)

总结一下,通过这个例子,我们应当理解:

  1. 编译链接的需求:允许引用其他文件(C标准成为编译单元,Compilation Unit)里定义的符号。C语言中不禁止你随便声明符号的类型,但是类型不匹配是Undefined Behavior。
  2. C语言中类型的概念:C语言中的其实是可以理解为没有类型的,在C语言的眼中只有内存和指针,也就是内存地址,而所谓的C语言中的类型,其实就是对这个地址的一个解读。

程序的编译 - 可重定向文件

我们先用file命令来查看main.c编译生成的main.o文件的属性:

file main.o

输出:

main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

我们看到这里的main.o文件是可重定向( relocatable) 的ELF文件,这里的重定向指的就是我们链接过程中对外部符号的引用。也就是说,编译过的main.o文件对于其中声明的外部符号如foox,y,是不知道的。

既然外部的符号是在链接时才会被main程序知道,那在编译main程序,生成可重定向文件时这些外部的符号是怎么处理的呢?我们同样通过objdump工具来查看编译出的main.o文件(未修改的原版本):

objdump -d main.o

输出:

在这里插入图片描述

main在编译的时候,引用的外部符号就只能 ”留空(0)“ 了。

我们看到,在编译但还未链接的main.o文件中,对于引用的外界符号的部分是用留空的方式用0暂时填充的。即上图中红框框出来的位置。注意图中的最后一列是笔者添加的注释,指明了本行中留空的地方对应那个外部符号。

另外注意这里的%rip相对寻址的偏移量都是0,一会儿我们会讲到,在静态链接完成之后,它们的偏移量会被填上正确的数值。

我们已经知道在编译时生成的文件中外部符号的部分使用0暂时留空的,这些外部符号是待链接时再填充的。那么,我们在链接时究竟需要填充哪些位置呢?我们可以使用readelf工具来查看ELF文件的重定位信息:

readelf -r main.o

在这里插入图片描述

这个图中上方是readelf的结果,下面是objdump的结果,笔者在这里已经将前两个外部符号的偏移量的对应关系用红色箭头指了出来,其他的以此类推。这种对应也可以证明我们上面的分析是正确的的。

应当讲,可重定向ELF文件(如main.o)已经告诉了我们足够多的信息,指示我们应该将相应的外部符号填充到哪个位置。

另外,注意%rip寄存器指向了当前指令的末尾,也就是下一条指令的开头,所以上图中最后的偏移量要减4(如 y - 4)。

程序的静态链接

简单讲,程序的静态链接是会把所需要的文件链接起来生成可执行的二进制文件,将相应的外部符号,填入正确的位置(就像我们上面查看的那样)。

我们可以通过使用gcc的 -Wl,--verbose--verbose传递给链接器ld,从而直接观察到整个静态链接的过程,包括:

  • ldscript里面各个section是按照何种顺序 “粘贴”
  • ctors / dtors (constructors / destructores) 的实现,( 我们用过__attribute__((contructor)) )
  • 只读数据和读写数据之间的padding,. = DATA_SEGMENT_ALIGN …

我们可以通过objdump来查看静态链接完成以后生成的可执行文件a.out的内容:

objdump -d a.out

在这里插入图片描述

注意,这个a.out的objdump结果图要与我们之前看到的main.o的objdump输出对比着来看。

我们可以看到,之前填0留空的地方都被填充上了正确的数值,%rip相对寻址的偏移量以被填上了正确的数值,而且objdump也能够正确地解析出我们的外部符号名(最后一列)的框。

静态ELF加载器:加载 a.out 执行

静态ELF文件的加载:将磁盘上静态链接的可执行文件按照ELF program header,正确地搬运到内存中执行。

操作系统在execve时完成:

  • 操作系统在内核态调用mmap
    • 进程还未准备好时,由内核直接执行 ”系统调用“
    • 映射好 a.out 的代码、数据、堆区、堆栈、vvar、vdso、vsyscall
  • 更简单的实现:直接读入进程的地址空间

加载完成之后,静态链接的程序就开始从ELF entry开始执行,之后就变成我们熟悉的状态机,唯一的行为就是取指执行。

我们通过readelf来查看a.out文件的信息:

readelf -h a.out

输出:

在这里插入图片描述

我们这里看到,程序的入口地址是:Entry point address: 0x400a80。我们接着用gdb来调试:

在这里插入图片描述

上图是笔者在gdb中调试的一些内容:

  1. 我们用starti来使得程序在第一条指令就停下,可以看到,程序确实是从0x400180开始的,与我们上面查到的入口地址一致。
  2. 而我们用cat /proc/[PID]/maps 来查看这个程序中内存的内容,看到我们之前提到的代码、数据、堆区、堆栈、vvar、vdso、vsyscall都已经被映射进了内存中。

调试的结果符合我们对静态程序加载时操作系统的行为的预期。

总结:静态链接与加载

  1. 由于我们不想将所有的代码都写到同一个文件中,所以我们需要分文件编写并编译,然后链接。
  2. 对于静态链接而言。编译某个文件时会将外部符号先留空为0,然后在静态链接时,将所有的留空的地方正确地进行填充。
  3. 在静态加载时,操作系统会调用mmap将我们运行这个程序所需要的代码、数据、堆区、堆栈、vvar、vdso、vsyscall等信息都映射进内存,然后从ELF entry的入口地址开始执行。

动态链接与加载

为什么需要动态链接/加载?

libc.so中有300K 条指令,2 MiB 大小,每个程序如果都静态链接,浪费的空间很大,最好是整个系统里只有一个 libc 的副本,而每个用到 libc 的程序在运行时都可以用到 libc 中的代码

  • 文件系统里只有一个副本 (libc.so)
  • 内存里只有一个副本

问题:真的整个操作系统里只有一个 libc 的副本吗?

  • 方法 1: 看 Linux Kernel 的 trace
  • 方法 2: 调试 Linux Kernel, 查看内存映射 (QEMU monitor)
  • 方法 3: 简单做个实验

实验验证

我们通过创建一个动态链接库 libhuge.so, 然后创建1000个进程去调用这个库中的foo函数,该函数是128M 个 nop。如果程序不是动态链接的话,1000 * 128MB的内存占用足以撑爆大多数个人电脑的内存。而如果程序确实是动态链接的,即内存中只有一份代码,那么只会有很小的内存占用。我们是这样做的:

首先我们有huge.S

.global foo
foo:# 128MiB of nop.fill 1024 * 1024 * 128, 1, 0x90ret

这就是我们刚才说的一个动态链接库的源代码。我们一会儿会把他编译成 libhuge.so供我们的huge.c调用,我们的huge.c是这样的:

#include <unistd.h>
#include <stdio.h>
int main(){foo(); // huge code, dynamic linkedprintf("pid = %d\n", getpid());while (1) sleep(1);
}

它会调用foo函数,并在结束后打印自己的PID,然后睡眠。Makefile如下:

LIB := /tmp/libhuge.soall: $(LIB) a.out$(LIB): huge.Sgcc -fPIC -shared huge.S -o $@a.out: huge.c $(LIB)gcc -o $@ huge.c -L/tmp -lhugeclean:rm -f *.so *.out $(LIB)

正如我们刚才所介绍的,我们会先将huge.S编译成动态链接库libhuge.so放在/tmp下,然后我们的huge.c回去动态链接这个库,并完成自己的代码。这还不够,我们要创建1000个进程来执行上述行为。这样才能验证我们的动态链接是不是在内存中真的只有一份代码,我们用下面的脚本来完成:

#!/bin/bash# for i in {1...1000}
for i in `seq 1 100`
doLD_LIBRARY_PATH=/tmp ./a.out &
donewait
# ps | grep "a.out" | grep -Po "^(\d)*" | xargs kill -9  用于清空生成的进程

实验证明,我们的操作系统能够很好地运行这1000个进程,并且内存只多占用了 400MB。也就是说,库中的foo函数确实是动态链接的,内存中只有一份foo的副本。

这在操作系统内核不难实现:所有以只读方式映射同一个文件的部分(如代码部分)时,都指向同一个副本,这个过程中会创建引用计数。

实现动态链接与加载

我们要实现动态链接,需要具体做到哪些事情呢?我们希望有一个库函数,其中包含一些代码,所有的进程链接这一段代码,这段代码在内存中只有一份拷贝。

实现动态加载(1)

需求1:加载纯粹的代码

编译成位置无关代码(Position Independent Code, PIC)即可,即引用代码(跳转)全部使用PC相对寻址。x86已经是这样了。直接把代码mmap进地址空间就行了。

# foo.S
.global fool
foo:movl $1, %eaxret

比如上面这段代码,它很简单,就是返回1。

loader.c

实现动态加载(2)

需求2:动态链接库只有纯粹的代码没有数据可不行,我们要能加载代码,并且代码有附带的数据。

这也好办,将代码和数据放在一起,然后都使用PC相对寻址就好了。

对于x86不支持rip相对寻址,我们可以通过 call __i686. get_pc_thunk.bx 来得到下条指令的地址。

我们有这样一段代码:

# bar.S
x:      # 数据不能共享 (MAP_PRIVATE 方式映射).int 0.global bar
bar:addl $1, x(%rip)movl x(%rip), %eaxret

这相当于这样一段C代码:

int bar(){static int x = 0;return ++x;
}

即在静态区定义一个变量x,然后每次调用bar函数时都会将x加一并返回。这也是一段位置无关代码,也可以直接mmap到内存中去执行。

实现动态加载(3)

需求3:比较难的是,一个文件或者一个动态链接库想要访问另外一个动态链接库导出的符号。因为我们想要知道的符号(比如bar)也是动态加载的,也就是说,符号的地址是运行(加载)的时候才能确定的。而我们在编译(比如编译baz时)的时候无法知道动态加载的符号bar的地址。即允许访问其他动态链接库导出的符号(代码 / 数据)。

解决方法是我们用一张表,编译时编译成:call *table[bar]bar.o会先被映射到进程的地址空间中,然后,我们要将baz.o映射到地址空间时,我们会给baz所保有的这张表中bar所对应的表项填上正确的数值,即此时已知的bar的地址。即我们为每个动态加载的符号(代码 / 数据)创建一张变,在运行时每次用到这些动态符号时,才解析符号的地址。

.global ..bar 
..bat: bar:.quad 0.global baz
baz:movq baz(%rip), %rdicall *%rdiret

重填(相当于在运行时重做静态链接),这样行吗?不行,因为这样违背了我们动态链接的初衷:希望整个内存中只有一份代码的副本,而每次重填会导致每次都在内存中多一份代码的副本。而上面的解决方案,只有这张表,是需要复制的,这大幅减少了系统中冗余的内存。

总结

总结一下,实现动态链接和加载就是两个关键点:

  1. PIC位置无关代码,不管是代码还是数据,我们全部都要通过PC相对寻址,来使得它们是位置无关代码。
  2. 要引用动态链接库中的符号(编译时不知道)时,我们创建一张表,在运行(加载)时将其填上正确的地址。

例子

假如我们是十几种有这样一个动态链接(共享代码)的需求:

  • main需要调用libc中的printf
  • printf需要调用libfoo中的foo

我们知道,动态加载的程序最先并不是从main的入口地址开始执行的。而是需要先由加载器libld进行动态加载。libld由操作系统加载,按照相互依赖相反的方向加载:

  1. libld加载libfoo,一切顺利
  2. libld加载libc
    • libcfoo的调用在编译时,被编译为call *libc.tab[FOO]
    • libld调用dl_runtime_resolve解析符号,填入libc.tab[FOO],因为此时libfoo已经被加载到地址空间中了,foo地址是已知的
  3. libld完成main的初始化
    • a.outprintf的调用在编译时,被编译成call *a.out.tab[PRINTF]
    • libld机械printf的地址,填入call *a.out.tab[PRINTF],因为此时libc已经被加载到地址空间中了,printf地址是已知的

所有的填表都完成之后,就跳转到main的入口地址开始执行。

ELF 动态链接与加载

上面一种简化版的动态加载过程,实际的ELF动态加载比这要复杂一点。

GOT (Global Offset Table)

GOT

GOT:shared object用来存储动态符号的表格。库函数有,可执行文件也有。

所以用file命令查看a.outlibc.so时都是 ”shared object“ 。也就是说我们生成的可执行文件其实和库函数是同一种文件格式。它们都需要调用其他的动态链接库中的符号。

GOT中储存的数据
  • GOT[0]:.dynamic节的地址
  • GOT[1]:link map,用于遍历依赖的动态链接库
  • GOT[2]:dl_runtime_resolve 的地址,即call *GOT[2] 可以完成符号解析
  • GOT[i]:程序所需的动态符号的地址(printf, …)

lazy symbol resolution 延迟符号解析

新需求:能否降低实际没有调用到的符号的开销?

程序可能会引用很多符号,但执行时可能大部分符号都没用到,逐个dl_runtime_resolve的话会造成不必要的开销。

lazy symbol resolution

想法:加载时设置为NULL,加载时来判断 / 解析

使用一小段 ”trampoline code“ 跳板代码

  • 如果符号还未解析,就解析
  • 跳转到解析后的符号执行
int print_internal(const char *fmt, ...){if (GOT[PRINRF]){GOT[PRINTF] = call_dl_runtime_reslove("printf");}return GOT[PRINTF]{...};
}

需要编译器把向printf(动态链接库)的调用翻译成call printf_internal

坏处:fast path多做一次判断:call + load + 判断 + jump,会损失一定的性能。

黑科技:让printf@GOT指向trampoline的下一条指令。

在这里插入图片描述

  • 只有两条指令:call print@plt; jmp *a.out.GOT[PRINTF]
  • 对现代处理器非常友好,因为有branch-target-buffer(BTB),几乎不损失性能。

Takeaways and Wrap-up

我们通过逐步把需求进行分解,从加载的视角理解链接:

  1. 需要加载一段代码(foo):PIC(通过使用PC相对寻址)+ mmap
  2. 代码需要伴随数据(bar):数据也使用PC相对寻址 + mmap
  3. 需要解析动态符号(baz):查表(GOT)、优化:PLT,lazy symbol resolve

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

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

相关文章

饥荒联机自建服务器有什么用,联机版饥荒使用专用服务器的好处 | 手游网游页游攻略大全...

发布时间&#xff1a;2016-02-15存档保存位置是?很多玩家对此并不是很了解,不过别着急哟,下面99单机小编就为你带来高玩分享的相关技巧心得攻略,希望大家能喜欢. 联机版的存档与单机版是不同的,由于联机版饥荒建 ...标签&#xff1a;游戏资讯 攻略秘籍发布时间&#xff1a;201…

(2021) 26 [持久化] 持久数据的可靠性:RAID和journaling

(2021) 26 [持久化] 持久数据的可靠性&#xff1a;RAID和journaling 南京大学操作系统课蒋炎岩老师网络课程笔记。 视频&#xff1a;https://www.bilibili.com/video/BV1HN41197Ko?p26 讲义&#xff1a;http://jyywiki.cn/OS/2021/slides/16.slides#/ 背景 回顾 文件系统 …

win10无法检验服务器出示的ssl证书,win10系统网站启用ssL安全证书的操作方法

win10系统网站启用ssL安全证书的操作方法?很多win10用户在使用电脑的时候&#xff0c;会发现win10系统网站启用ssL安全证书的的现象&#xff0c;根据小编的调查并不是所有的朋友都知道win10系统网站启用ssL安全证书的的问题怎么解决&#xff0c;不会的朋友也不用担心&#xff…

Nplayer本地文件拷到服务器,手把手教你简易NAS构建,手机/平板/智能电视随意调取,家庭存储云共享,有了自己的网络云盘后再也不用担心容量不够了!...

之前嫌键盘侠烦&#xff0c;写这些也没意义所以把账号注销了文章删除了&#xff0c;现在想了想我抗吧12级老蛆还喷不过这帮小兔崽子&#xff1f;换了skt.ruo秽土转生&#xff0c;求喷子和我在各评论对线。特别是匿名dog见一个怼死一个。下面是之前号写的内容原文 -#简介NAS全称…

gdb 入门

gdb 入门 简介 gdb是GNU开源组织发布的一个强大的Linux下的程序调试工具。 一般来说&#xff0c;GDB主要帮助你完成下面四个方面的功能&#xff1a; 1、启动你的程序&#xff0c;可以按照你的自定义的要求随心所欲的运行程序。 2、可让被调试的程序在你所指定的调置的断点…

Linux下的CUDA多版本管理

Linux下的CUDA多版本管理 关于CUDA、cuDNN等的简介和安装可参考&#xff1a;显卡、显卡驱动、CUDA、CUDA Toolkit、cuDNN 梳理。 CUDA多版本 有时我们会在一台机器上同时看到多个版本的CUDA&#xff0c;比如nvcc -V和nvidia-smi的输出就可能会不同&#xff1a; 在我们实验室…

ONNX初探

ONNX初探 转载自&#xff1a;https://blog.csdn.net/just_sort/article/details/112912272 0x0. 背景 最近看了一些ONNX的资料&#xff0c;一个最大的感受就是这些资料太凌乱了。大多数都是在介绍ONNX模型转换中碰到的坑点以及解决办法。很少有文章可以系统的介绍ONNX的背景…

服务器修改地址,服务器修改管理地址

服务器修改管理地址 内容精选换一换在弹性云服务器上安装完成后输入公网IP&#xff0c;无法连接目的虚拟机&#xff0c;端口无法访问工具。源端网络未连通目的端。目的端安全组未开放8084端口。目的端网络ACL禁用了8084端口。登录源端服务器后&#xff0c;在源端服务器中ping 目…

ONNX再探

ONNX再探 本文转自&#xff1a;https://blog.csdn.net/just_sort/article/details/113802330 这篇文章从多个角度探索了ONNX&#xff0c;从ONNX的导出到ONNX和Caffe的对比&#xff0c;以及使用ONNX遭遇的困难以及一些解决办法&#xff0c;另外还介绍了ONNXRuntime以及如何基于…

图解自监督学习(CV)

图解自监督学习&#xff08;CV&#xff09; 译自&#xff1a;https://amitness.com/2020/02/illustrated-self-supervised-learning/ 作者&#xff1a;Amit Chaudhary 注&#xff1a;译者在某些地方对原文的表述做了调整&#xff0c;使其适合汉语的阅读习惯&#xff0c;并在…

机器学习中的归纳偏置

机器学习中的归纳偏置 带着偏见看世界&#xff0c;否则你根本没有看待世界的方式。 本文主要参考整理自知乎问题&#xff1a;如何理解Inductive bias&#xff1f; No-Free-Lunch&#xff08;NLF&#xff09;定理指出学习是不可能的&#xff0c;除非有先验知识。通常情况下&…

【c语言数据结构笔记】1.2 数据结构

1.2数据结构 数据元素并独立 结构实体关系 形式定义&#xff08;D&#xff0c;S&#xff09; 其中D是数据元素的有限集&#xff0c;S是D上关系的有限集 eg&#xff1a;12位数&#xff1a;132423451233 分成三组四位数 次序关系<a1,a2><a2,a3> 遵守次序关系 eg&…

使用Apex进行混合精度训练

使用Apex进行混合精度训练 转自&#xff1a;https://fyubang.com/2019/08/26/fp16/ 你想获得双倍训练速度的快感吗&#xff1f; 你想让你的显存空间瞬间翻倍吗&#xff1f; 如果我告诉你只需要三行代码即可实现&#xff0c;你信不&#xff1f; 在这篇博客里&#xff0c;瓦砾…

【数据结构1.3笔记】研究内容

1.3研究内容 数据结构&#xff08;D&#xff0c;S&#xff09; {逻辑结构&#xff1a; {物理结构&#xff08;存储结构&#xff09; {数据的运算 1.逻辑结构 1 集合&#xff1a;集合&#xff0c;没有逻辑关系 2 线性结构 “一对一” 3树形结构 层次关系 4图形结构 练习&…

2019年蓝桥杯第一题

第一题 标题&#xff1a;组队&#xff08;本题总分&#xff1a;5 分&#xff09; 作为篮球队教练&#xff0c;你需要从以下名单中选出 1 号位至 5 号位各一名球员&#xff0c; 组成球队的首发阵容。 每位球员担任 1 号位至 5 号位时的评分如下表所示。请你计算首发阵容 1 号位…

深度学习编译:MLIR初步

深度学习编译MLIR初步 深度模型的推理引擎 目前深度模型的推理引擎按照实现方式大体分为两类&#xff1a;解释型推理引擎和编译型推理引擎。 解释型推理引擎 一般包含模型解析器&#xff0c;模型解释器&#xff0c;模型优化器。 模型解析器负责读取和解析模型文件&#xff…

深入浅出LLVM

深入浅出LLVM 转自&#xff1a;https://www.jianshu.com/p/1367dad95445 什么是LLVM&#xff1f; LLVM项目是模块化、可重用的编译器以及工具链技术的集合。 美国计算机协会 (ACM) 将其2012 年软件系统奖项颁给了LLVM&#xff0c;之前曾经获得此奖项的软件和技术包括:Java、A…

一分钟系列:什么是虚拟内存?

一分钟系列&#xff1a;什么是虚拟内存&#xff1f; 转自&#xff1a;https://mp.weixin.qq.com/s/opMgZrXV-lfgOWrNUMKweg 注&#xff1a;一分钟系列的篇幅都不长&#xff0c;适合吃饭蹲坑、地铁公交上食用&#xff5e; 内存对于用户来说就是一个字节数组&#xff0c;我们可…

11-Kafka

1 Kafka Kafka是一个分布式流式数据平台&#xff0c;它具有三个关键特性 Message System: Pub-Sub消息系统Availability & Reliability&#xff1a;以容错及持久化的方式存储数据记录流Scalable & Real time 1.1 Kafka架构体系 Kafka系统中存在5个关键组件 Producer…

虚拟内存精粹

虚拟内存精粹 标题&#xff1a;虚拟内存精粹 作者&#xff1a;潘建锋 原文&#xff1a;HTTPS://strikefreedom.top/memory-management–virtual-memory 导言 虚拟内存是当今计算机系统中最重要的抽象概念之一&#xff0c;它的提出是为了更加有效地管理内存并且降低内存出错的概…