动态库的入口——VCRT(DLL)和CRT(SO)

news2024/12/29 9:05:43

  摘要:为了更加深入的理解动态库的加载初始化过程,本文根据VCRT和Linux-CRT的代码实现详细描述了windows和linux平台下对应动态库加载时会进行哪些工作。本文重点关注全局变量的初始化时机,以及是否有其他额外的操作。
  关键字:dll,so,全局变亮,vcrt,crt
  读者须知:读者需要有基本的C/C++编程能力,了解C++的二进制可执行文件的生成过程以及生成文件的组成与加载流程。

  可执行文件执行用户代码的入口时main函数,在CRT和glibc的实现中其具体入口各不相同。而动态库是另一种形式的可执行文件,其需要被加载到一个已经运行起来的进程的进程空间进行执行。动态库作为可执行的二进制文件其本身也存在全局变量这些需要在程序运行前初始化和程序结束后析构的工作,那么动态库是否也有入口?
  本文根据CRT和llvm-cxx的实现详细解析动态库的入口和全局变量等的构造析构工作的执行时机。

1 VCRT动态库入口

1.1 DllMain

  CRT的动态库即DLL,其入口为DllMain,下面是MSN上关于DllMain的描述:

DllMain:动态链接库 (DLL) 的可选入口点。 当系统启动或终止进程或线程时,它将使用进程的第一个线程为每个加载的 DLL 调用入口点函数。 使用 LoadLibrary 和 FreeLibrary 函数加载或卸载 DLL 时,系统还会调用 DLL 的入口点函数。

  我们看一个简单的例子。下面是动态库的代码:

struct __declspec(dllexport) myclass {
public:
	myclass() {
		std::cout << this << std::endl;
		std::cout << "the myclass class is created" << std::endl;
	}

	~myclass() {
		std::cout << this << std::endl;
		std::cout << "the myclass class is destroyed" << std::endl;
	}
};


myclass cls{};

  下面是动态加载的代码:

int main(){
	const auto handle = LoadLibrary(L"add.dll");
	if (handle) {
		std::cout << "delete dll" << std::endl;
		FreeLibrary(handle);
		std::cout << "dll has been deleted" << std::endl;
	}

	return 0;
}

  执行文的输出为:

519AE138
the myclass class is created
the dll main 1
delete dll
the dll main 0
519AE138
the myclass class is destroyed
dll has been deleted

  从上面的行为中我们可以看出DllMain的行为和main函数类似,全局变量的初始化和析构都是在动态库加载和卸载时发生的。

1.2 CRT Dll实现

  在上面的实验中我们已经理解Dll的入口和全局变量的初始化时机,我们简单看下DllMain调用之前的CRT实现,具体代码直接从VS的VCRuntime中找就可以。其大致的流程如下,_DllMainCRTStartup->dllmain_dispatch->DllMain,如论动态库的加载还是卸载都会调用该函数。
在这里插入图片描述

_DllMainCRTStartup
  _DllMainCRTStartup本身没有什么,就是dllmain_dispatch的包装,我们具体看下dllmain_dispatch中都做了哪些工作。
  前三行就是检查当前动态库已经被加载的数量,而__proc_attached是一个全局静态变量,表明当前动态库的引用计数。
  下面的代码都是调用了dllmain_crt_dispatchdllmain_raw,如果是attach二者的调用顺序为dllmain_raw->dllmain_crt_dispatch;如果为detach为dllmain_crt_dispatch->dllmain_raw

static BOOL __cdecl dllmain_dispatch(
    HINSTANCE const instance,
    DWORD     const reason,
    LPVOID    const reserved
    )
{
    // If this is a process detach notification, check that there was a prior
    // process attach notification that was processed successfully.  This is
    // to ensure that we don't detach more times than we attach.
    if (reason == DLL_PROCESS_DETACH && __proc_attached <= 0)
    {
        return FALSE;
    }

    BOOL result = TRUE;
    __try
    {
        if (reason == DLL_PROCESS_ATTACH || reason == DLL_THREAD_ATTACH)
        {
            result = dllmain_raw(instance, reason, reserved);
            if (!result)
                __leave;

            result = dllmain_crt_dispatch(instance, reason, reserved);
            if (!result)
                __leave;
        }

        result = DllMain(instance, reason, reserved);

        // If the client DllMain routine failed, unwind the initialization:
        if (reason == DLL_PROCESS_ATTACH && !result)
        {
            DllMain(instance, DLL_PROCESS_DETACH, reserved);
            dllmain_crt_dispatch(instance, DLL_PROCESS_DETACH, reserved);
            dllmain_raw(instance, DLL_PROCESS_DETACH, reserved);
        }

        if (reason == DLL_PROCESS_DETACH || reason == DLL_THREAD_DETACH)
        {
            result = dllmain_crt_dispatch(instance, reason, reserved);
            if (!result)
                __leave;

            result = dllmain_raw(instance, reason, reserved);
            if (!result)
                __leave;
        }
    }
    __except(__scrt_dllmain_exception_filter(
        instance,
        reason,
        reserved,
        dllmain_crt_dispatch,
        GetExceptionCode(),
        GetExceptionInformation()))
    {
        result = FALSE;
    }

    return result;
}

dllmain_raw
  dllmain_raw内部只调用了_pRawDllMain,这是一个外部符号,如果用户自己定义了则动态库加载前会先调用这个函数,否则什么也不做。比如:

BOOL WINAPI my_scrt_dllmain_type(HINSTANCE a , DWORD b, LPVOID c) {
	printf("the dll main my_scrt_dllmain_type\n");
	return TRUE;
}

extern "C" __scrt_dllmain_type const _pRawDllMain = my_scrt_dllmain_type;

dllmain_crt_process_attach
  dllmain_crt_process_attach内部根据当前的状态不同分别调用dllmain_crt_process_attach,dllmain_crt_process_detach,__scrt_dllmain_crt_thread_attach,__scrt_dllmain_crt_thread_detach,我们主要关注前两个函数,对其他细节不必太深究。

  下面的代码会用注释的方式来简单梳理下调用流程:

static BOOL __cdecl dllmain_crt_process_attach(HMODULE const instance,
    LPVOID const reserved)
{
    if (!__scrt_initialize_crt(__scrt_module_type::dll))
        return FALSE;
    //请求锁
    bool const is_nested = __scrt_acquire_startup_lock();
    bool fail = true;
    __try
    {
        if (__scrt_current_native_startup_state != __scrt_native_startup_state::uninitialized)
            __scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);

        __scrt_current_native_startup_state = __scrt_native_startup_state::initializing;
        //内部会调用_initialize_onexit_table分别初始化module_local_atexit_table和module_local_at_quick_exit_table两个DLL卸载时的函数指针表格,一般为全局变量的析构函数,用户也可以主动注册。
        if (!__scrt_dllmain_before_initialize_c())
            __leave;

        #ifdef _RTC
        _RTC_Initialize();
        #endif
        //初始化全局的typeinfo链表
        __scrt_initialize_type_info();
        //初始化标准输入输出的一些基本的选项,基本不同关心
        __scrt_initialize_default_local_stdio_options();
        //和exe相同遍历C的初始化函数指针表格,一般为全局变量的初始化函数
        if (_initterm_e(__xi_a, __xi_z) != 0)
            __leave;
        //环境变量之类的初始化
        if (!__scrt_dllmain_after_initialize_c())
            __leave;
        //遍历C++的初始化函数指针表格,一般为全局变量的构造函数之类
        _initterm(__xc_a, __xc_z);

        __scrt_current_native_startup_state = __scrt_native_startup_state::initialized;
        fail = false;
    }
    __finally
    {   //释放锁
        __scrt_release_startup_lock(is_nested);
    }
    if (fail)
        return FALSE;

    // If we have any dynamically initialized __declspec(thread) variables, we
    // invoke their initialization for the thread on which the DLL is being
    // loaded.  We cannot rely on the OS performing the initialization with the
    // DLL_PROCESS_ATTACH notification because, on Windows Server 2003 and below,
    // that call happens before the CRT is initialized.
    PIMAGE_TLS_CALLBACK const* const tls_init_callback = __scrt_get_dyn_tls_init_callback();
    if (*tls_init_callback != nullptr && __scrt_is_nonwritable_in_current_image(tls_init_callback))
    {
        (*tls_init_callback)(instance, DLL_THREAD_ATTACH, reserved);
    }
    //引用计数+1
    ++__proc_attached;
    return TRUE;
}

  dllmain_crt_process_detach就和attach基本类似只不过调用顺序上相反而已。

1.3 DLL调用时序

  从上面的实现中,我们简单整理下DLL加载和卸载时的调用顺序:
在这里插入图片描述

  从上面的时序中可以看出我们有两个入口dllmain_rawDllMain,前者是动态库刚进内存什么也没做,后者是已经完成初始化工作了,出口类似。

2 Linux CRT动态库入口

2.1 SO CRT入口

  在详细介绍Linux CRT动态库加载的函数执行之前我们先看个例子:

//add.h
#pragma once
#include <cstdio>
class A{
public:
    A();
    ~A();
};

__attribute__((visibility ("default"))) void func();

//add.cpp
#include "add.h"
A a;
A::A(){
    printf("a class constructor function \n");
}

A::~A(){
    printf("a class destructor function\n");
}

void func(){
    printf("func\n");
}

//a.h
#pragma once
__attribute__ ((constructor)) void foo(void);
__attribute__((destructor))void after();
//a.cpp
#include "a.h"
#include <cstdio>
class B{
    public:
        B(){
            printf("B is contructor");
        }
};

B b;
void foo(void){
        printf("foo is running and printf is available at this point\n");
}

void after(){
     printf("this is my custome after destructor\n");
}
//main.cpp
#include <stdlib.h>
#include <dlfcn.h>
#include <stdio.h>

typedef void (*func)();

int main(int argc, char **argv){
    const char *f = "./libadd.so";
    auto p = dlopen(f, RTLD_NOW);
    printf("load library %p \n%s\n", p, dlerror());
    dlclose(p);
    printf("the lib is unload\n");
    return 0;
}

  四个文件链接为一个动态库然后动态加载执行的结果:

a class constructor function
foo is running and printf is available at this point
B is contructorload library
this is my custome after destructor
a class destructor function
the lib is unload

  动态库文件的反汇编这里就不贴了太长了,基本结构和可执行文件差不多。然后我们在类B的构造函数处断点下看看堆栈:

#0  B::B (this=0x7ffff7fc805a <b>) at a.cpp:6
#1  0x00007ffff7fc50f0 in __cxx_global_var_init () at a.cpp:10
#2  0x00007ffff7fc5109 in _GLOBAL__sub_I_a.cpp () from ./libadd.so
#3  0x00007ffff7fe0b9a in call_init (l=<optimized out>, argc=argc@entry=1, argv=argv@entry=0x7fffffffe258, env=env@entry=0x7fffffffe268) at dl-init.c:72
#4  0x00007ffff7fe0ca1 in call_init (env=0x7fffffffe268, argv=0x7fffffffe258, argc=1, l=<optimized out>) at dl-init.c:30
#5  _dl_init (main_map=0x416ed0, argc=1, argv=0x7fffffffe258, env=0x7fffffffe268) at dl-init.c:119
#6  0x00007ffff7bd1985 in __GI__dl_catch_exception (exception=<optimized out>, operate=<optimized out>, args=<optimized out>) at dl-error-skeleton.c:182
#7  0x00007ffff7fe50cf in dl_open_worker (a=a@entry=0x7fffffffded0) at dl-open.c:758
#8  0x00007ffff7bd1928 in __GI__dl_catch_exception (exception=<optimized out>, operate=<optimized out>, args=<optimized out>) at dl-error-skeleton.c:208
#9  0x00007ffff7fe460a in _dl_open (file=0x402004 "./libadd.so", mode=-2147483646, caller_dlopen=<optimized out>, nsid=-2, argc=1, argv=0x7fffffffe258, env=0x7fffffffe268) at dl-open.c:837
#10 0x00007ffff7fb034c in dlopen_doit (a=a@entry=0x7fffffffe0f0) at dlopen.c:66
#11 0x00007ffff7bd1928 in __GI__dl_catch_exception (exception=exception@entry=0x7fffffffe090, operate=<optimized out>, args=<optimized out>) at dl-error-skeleton.c:208
#12 0x00007ffff7bd19f3 in __GI__dl_catch_error (objname=0x7ffff7fb40f0 <last_result+16>, errstring=0x7ffff7fb40f8 <last_result+24>, mallocedp=0x7ffff7fb40e8 <last_result+8>, operate=<optimized out>, args=<optimized out>) at dl-error-skeleton.c:227
#13 0x00007ffff7fb0b59 in _dlerror_run (operate=operate@entry=0x7ffff7fb02f0 <dlopen_doit>, args=args@entry=0x7fffffffe0f0) at dlerror.c:170
#14 0x00007ffff7fb03da in __dlopen (file=<optimized out>, mode=<optimized out>) at dlopen.c:87
#15 0x0000000000401192 in main (argc=1, argv=0x7fffffffe258) at main.cpp:9

  从上面的堆栈我们能够清晰的看到初始化函数的调用实际以及反汇编中看到析构函数的注册实际和构造函数调用时机相同。

#0  A::~A (this=0x7ffff7fc8059 <a>) at add.cpp:9
#1  0x00007ffff7ab7fde in __cxa_finalize (d=0x7ffff7fc8048) at cxa_finalize.c:83
#2  0x00007ffff7fc51a7 in __do_global_dtors_aux () from ./libadd.so
#3  0x00007ffff7fc7dc8 in __frame_dummy_init_array_entry () from ./libadd.so
#4  0x00007ffff7fe5952 in call_destructors (closure=closure@entry=0x416ed0) at dl-close.c:125
#5  0x00007ffff7bd1985 in __GI__dl_catch_exception (exception=<optimized out>, operate=<optimized out>, args=<optimized out>) at dl-error-skeleton.c:182
#6  0x00007ffff7fe5f36 in _dl_close_worker (map=map@entry=0x416ed0, force=force@entry=false) at dl-close.c:297
#7  0x00007ffff7fe6f05 in _dl_close_worker (force=false, map=0x416ed0) at dl-close.c:145
#8  _dl_close (_map=0x416ed0) at dl-close.c:859
#9  0x00007ffff7bd1928 in __GI__dl_catch_exception (exception=exception@entry=0x7fffffffe0c0, operate=<optimized out>, args=<optimized out>) at dl-error-skeleton.c:208
#10 0x00007ffff7bd19f3 in __GI__dl_catch_error (objname=0x7ffff7fb40f0 <last_result+16>, errstring=0x7ffff7fb40f8 <last_result+24>, mallocedp=0x7ffff7fb40e8 <last_result+8>, operate=<optimized out>, args=<optimized out>) at dl-error-skeleton.c:227
#11 0x00007ffff7fb0b59 in _dlerror_run (operate=operate@entry=0x7ffff7fb0420 <dlclose_doit>, args=0x416ed0) at dlerror.c:170
#12 0x00007ffff7fb0468 in __dlclose (handle=<optimized out>) at dlclose.c:46
#13 0x00000000004011c4 in main (argc=1, argv=0x7fffffffe258) at main.cpp:11

2.2 so加载实现

  dlopen的时候会遍历每个section的初始化段,然后在call_init中遍历初始化函数,比如全局变量的构造函数。从上面的反汇编可以看到编译器为每个编译单元都生成了对应的seciton其名字为_GLOBAL__sub_I_***.cpp,dlopen时需要遍历每个编译单元然后依次调用每个编译单元中的对应的构造函数和标记为需要在load时运行的函数。

void
_dl_init (struct link_map *main_map, int argc, char **argv, char **env)
{
  ElfW(Dyn) *preinit_array = main_map->l_info[DT_PREINIT_ARRAY];
  ElfW(Dyn) *preinit_array_size = main_map->l_info[DT_PREINIT_ARRAYSZ];
  unsigned int i;

  if (__glibc_unlikely (GL(dl_initfirst) != NULL))
    {
      call_init (GL(dl_initfirst), argc, argv, env);
      GL(dl_initfirst) = NULL;
    }

  /* Don't do anything if there is no preinit array.  */
  if (__builtin_expect (preinit_array != NULL, 0)
      && preinit_array_size != NULL
      && (i = preinit_array_size->d_un.d_val / sizeof (ElfW(Addr))) > 0)
    {
      ElfW(Addr) *addrs;
      unsigned int cnt;

      if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS))
	_dl_debug_printf ("\ncalling preinit: %s\n\n",
			  DSO_FILENAME (main_map->l_name));

      addrs = (ElfW(Addr) *) (preinit_array->d_un.d_ptr + main_map->l_addr);
      for (cnt = 0; cnt < i; ++cnt)
	((init_t) addrs[cnt]) (argc, argv, env);
    }

  /* Stupid users forced the ELF specification to be changed.  It now
     says that the dynamic loader is responsible for determining the
     order in which the constructors have to run.  The constructors
     for all dependencies of an object must run before the constructor
     for the object itself.  Circular dependencies are left unspecified.

     This is highly questionable since it puts the burden on the dynamic
     loader which has to find the dependencies at runtime instead of
     letting the user do it right.  Stupidity rules!  */

  i = main_map->l_searchlist.r_nlist;
  while (i-- > 0)
    call_init (main_map->l_initfini[i], argc, argv, env);

#ifndef HAVE_INLINED_SYSCALLS
  /* Finished starting up.  */
  _dl_starting_up = 0;
#endif
}

  dlclose时类似:

static void
call_destructors (void *closure)
{
  struct link_map *map = closure;

  if (map->l_info[DT_FINI_ARRAY] != NULL)
    {
      ElfW(Addr) *array =
	(ElfW(Addr) *) (map->l_addr
			+ map->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
      unsigned int sz = (map->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
			 / sizeof (ElfW(Addr)));

      while (sz-- > 0)
	((fini_t) array[sz]) ();
    }

  /* Next try the old-style destructor.  */
  if (map->l_info[DT_FINI] != NULL)
    DL_CALL_DT_FINI (map, ((void *) map->l_addr
			   + map->l_info[DT_FINI]->d_un.d_ptr));
}

参考文献

  • DllMain
  • quick-exit

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

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

相关文章

被微服务循环依赖调用坑了 !

最近的迭代转测后&#xff0c;遇到了一个比较有意思的问题。系统在测试环境整体运行还算平稳&#xff0c;但是过一段时间之后&#xff0c;就开始有接口超时了&#xff0c;日志中出现非常多的 “java.net.SocketTimeoutException: Read timed out”。 试了几次重启大法&#xf…

用魔法打败魔法!用AI制作AI分割数据集!

本节内容我们使用SAM将边界框转换为分割数据集&#xff0c;这对于实例分割数据集的制作非常有用&#xff0c;下面我会一步步给出我的代码&#xff0c;希望对你有用。 有兴趣的朋友可以研究一下这本书&#xff0c;详细的介绍了数据集制作到分割的实际项目应用&#xff01; 步骤 …

【 计算机组成原理 】期末重点

文章目录 前言第一章 【计算机系统概论】1.1 知识点1.1核心例题 第二章 【运算方法和运算器】2.1 知识点2.2 核心例题 第三章 【存储系统】3.1 知识点3.2 核心例题 第四章 【指令系统】4.1 知识点4.2 核心例题 第五章 【中央处理器】5.1 知识点5.2 核心例题 第六章6.1 知识点6.…

【MSP432电机驱动学习—上篇】TB6612带稳压电机驱动模块、MG310电机、霍尔编码器

所用控制板型号&#xff1a;MSP432P401r 今日终于得以继续我的电赛小车速通之路&#xff1a; 苏轼云 “ 素面常嫌粉涴 &#xff0c; 洗妆不褪朱红。 ” 这告诫我们不能只注重在表面粉饰虚伪的自己&#xff0c;要像梅花一样&#xff0c;不断磨砺自己的内在~ 后半句是 “…

JavaSE基础语法--类和对象

在Java中&#xff0c;一切皆为对象&#xff0c;类和对象是一个抽象的概念。我们可以从面向过程来过渡到面向对象。 那么什么是面向过程呢&#xff1f; 举一个简单的例子&#xff0c;现实生活中&#xff0c;你需要买一台手机的时候会经历如下步骤&#xff1a; 这里的每一步都可…

【python程序设计】——期末大作业

【python程序设计】——期末大作业&#x1f60e; 前言&#x1f64c;一、所用技术&#xff1a;二、 系统设计三、 系统实现3.1 核心功能代码实现&#xff1a;3.2 演示结果展示 总结撒花&#x1f49e; &#x1f60e;博客昵称&#xff1a;博客小梦 &#x1f60a;最喜欢的座右铭&am…

yaffs格式的根文件系统制作

linux内核启动后&#xff0c;它接下来要做的事就是启动应用程序&#xff0c;而应用程序在哪里呢&#xff0c;类比windows&#xff0c;启动时要读取c盘&#xff0c;所以linux的文件系统就类似于c盘&#xff0c;并且我们使用的ls、cp等一些类命令&#xff08;本质是应用程序&…

JavaScript 手写代码 第二期

文章目录 1.为什么要手写代码&#xff1f;2. 手写代码2.1 手写实现判断类型函数2.1.1 前置知识2.1.1 手写实现 2.2 手写实现aplly,call,bind方法2.2.1 基本使用2.2.2 实现思路2.2.3 手写实现 1.为什么要手写代码&#xff1f; 我们在日常开发过程中&#xff0c;往往都是取出来直…

Linux(centos7)缺失.bashrc文件登录出现bash-4.2

一、问题描述 最近遇到几次登陆linux&#xff08;centos7.5&#xff09;系统后&#xff0c;虽然在/root用户下&#xff0c;但出现了如下界面&#xff1a; 二、解决思路 使用不同的linux发行版本&#xff0c;&#xff08;比如&#xff1a;IP为*...90,以下简称90&#xff09;会…

课程19:个人中心功能与提示优化

🚀前言 本文是《.Net Core从零学习搭建权限管理系统》教程专栏的课程(点击链接,跳转到专栏主页,欢迎订阅,持续更新…) 专栏介绍:以实战为线索,基于.Net 7 + REST + Vue、前后端分离,不依赖任何第三方框架,从零一步一步讲解权限管理系统搭建。 专栏适用于人群:We…

Android中加载一张大图,如何正常显示且不发生OOM ?

问题 在Android中&#xff0c;获取一个1000*20000(宽1000px&#xff0c;高20000px)的大图&#xff0c;如何正常加载显示且不发生OOM呢? 分析 Android系统会为应用分配一定大小的堆内存 而如果遇到高分辨率图片时&#xff0c;如果它的配置为ARGB(每个像素占4Byte) 那么它要消…

深度学习(23)——YOLO系列(2)

深度学习&#xff08;23&#xff09;——YOLO系列&#xff08;2&#xff09; 文章目录 深度学习&#xff08;23&#xff09;——YOLO系列&#xff08;2&#xff09;1. model2. dataset3. utils4. test/detect5. detect全过程 今天先写YOLO v3的代码&#xff0c;后面再出v5&…

【PCB专题】案例:PCB板厂说焊盘宽度太小容易沉金不良,但加宽又可能导致阻焊桥在阻焊为黑色油墨下无法做出?

此案例是最近在Layout一块PCB板卡,使用了一个以前我没有接触过的器件,此器件的封装是QFN128。 总的问题是:PCB板厂说如果按原稿制作的话,焊盘宽度太小沉金容易不良,但电话里和我说如果加宽又可能导致阻焊桥在黑色油墨情况下无法做出? 板厂给的EQ如下所示,表示TOP面设计…

【Unity之IMGUI】—编译模式下控件可视化及其封装

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

【从零开始开发一个线上网课系统-01】账号登录及退出登录功能开发

文章目录 1 视图层开发2 form表单验证3 配置urls.py4 模板层开发 实际上在系统开发的博客中应该先描述数据库设计&#xff0c;但由于设计的表比较多&#xff0c;其理解简单&#xff0c;但撰写和描述较为麻烦&#xff0c;所以我以可视化方式来呈现这些数据表以及其中的关系&…

RISCV Reader笔记_1 RISCV的意义

RISCV Reader RISCV的诞生 出众之处 RISCV架构被设计的目的就是成为一个通用的指令集架构 ISA。不仅支持从微控制器到高性能计算机的各种处理器&#xff0c;兼容各种编程语言&#xff0c;还适应FPGA ASIC等所有实现技术&#xff0c;稳定…… 计算机体系结构为了在指令集更新…

Iceberg从入门到精通系列之一:Iceberg核心概念理解

Iceberg从入门到精通系列之一&#xff1a;Iceberg核心概念理解 一、Iceberg核心概念二、Iceberg表结构三、数据文件四、表快照Snapshot五、清单列表Manifest list六、表快照、数据文件和清单列表之间的关系七、Catalog八、Hive Catalog九、Hadoop Catalog十、Hive Catalog和Had…

Alamouti,MRC以及beam三种方式的误码率对比MATLAB仿真程序

Alamouti,MRC以及beam三种方式的误码率对比MATLAB仿真程序 完整程序: clc; clear; close all; warning off; addpath(genpath(pwd)); %%%%%%%%%%%%%%%%%%%%%%%%% Initialization %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% N = 10^6; r_n = rand(1,N)>0.5; BPSK = 2*r_n-1; E_n_d…

chatgpt赋能python:Python生成GUI的步骤和工具

Python生成GUI的步骤和工具 Python是一种广泛使用的编程语言&#xff0c;其语法简洁、易学、可读性强等特点深受程序员喜爱。 Python的GUI编程让我们可以为用户提供友好的界面&#xff0c;帮助用户更好地理解和使用程序。Python生成GUI的过程并不复杂&#xff0c;本文将为您介…

代码随想录算法训练营第42天 | 01背包问题理论基础 + 416.分割等和子集

今日任务 目录 01背包问题 二维数组 01背包问题 一维/滚动数组 416.分割等和子集 - Medium 01背包问题 二维数组 理论基础&#xff1a;代码随想录 01 背包 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品…