uboot环境变量实现分析
u-boot的环境变量用来存储一些经常使用的参数变量,uboot希望将环境变量存储在静态存储器中(如nand nor eeprom mmc)。
其中有一些也是大家经常使用,有一些是使用人员自己定义的,更改这些名字会出现错误,下面的表中我们列出了一些常用的环境变量:
bootdelay 执行自动启动的等候秒数
baudrate 串口控制台的波特率
netmask 以太网接口的掩码
ethaddr 以太网卡的网卡物理地址
bootfile 缺省的下载文件
bootargs 传递给内核的启动参数
bootcmd 自动启动时执行的命令
serverip 服务器端的ip地址
ipaddr 本地ip地址
stdin 标准输入设备
stdout 标准输出设备
stderr 标准出错设备
上面这些是uboot默认存在的环境变量,uboot本身会使用这些环境变量来进行配置。我们可以自己定义一些环境变量来供我们自己uboot驱动来使用。
Uboot环境变量的设计逻辑是在启动过程中将env从静态存储器中读出放到RAM中,之后在uboot下对env的操作(如printenv editenv setenv)都是对RAM中env的操作,只有在执行saveenv时才会将RAM中的env重新写入静态存储器中。
这种设计逻辑可以加快对env的读写速度。
基于这种设计逻辑,2014.4版本uboot实现了saveenv这个保存env到静态存储器的命令,而没有实现读取env到RAM的命令。
那我们就来看一下uboot中env的数据结构 初始化 操作如何实现的。
一 env数据结构
在include/environment.h中定义了env_t,如下:
- #ifdef CONFIG_SYS_REDUNDAND_ENVIRONMENT
- # define ENV_HEADER_SIZE (sizeof(uint32_t) + 1)
- # define ACTIVE_FLAG 1
- # define OBSOLETE_FLAG 0
- #else
- # define ENV_HEADER_SIZE (sizeof(uint32_t))
- #endif
- #define ENV_SIZE (CONFIG_ENV_SIZE - ENV_HEADER_SIZE)
- typedef struct environment_s {
- uint32_t crc; /* CRC32 over data bytes */
- #ifdef CONFIG_SYS_REDUNDAND_ENVIRONMENT
- unsigned char flags; /* active/obsolete flags */
- #endif
- unsigned char data[ENV_SIZE]; /* Environment data */
- } env_t;
#ifdef CONFIG_SYS_REDUNDAND_ENVIRONMENT
# define ENV_HEADER_SIZE (sizeof(uint32_t) + 1)
# define ACTIVE_FLAG 1
# define OBSOLETE_FLAG 0
#else
# define ENV_HEADER_SIZE (sizeof(uint32_t))
#endif
#define ENV_SIZE (CONFIG_ENV_SIZE - ENV_HEADER_SIZE)
typedef struct environment_s {uint32_t crc; /* CRC32 over data bytes */
#ifdef CONFIG_SYS_REDUNDAND_ENVIRONMENTunsigned char flags; /* active/obsolete flags */
#endifunsigned char data[ENV_SIZE]; /* Environment data */
} env_t;
CONFIG_ENV_SIZE是我们需要在配置文件中配置的环境变量的总长度。
这里我们使用的nand作为静态存储器,nand的一个block是128K,因此选用一个block来存储env,CONFIG_ENV_SIZE为128K。
Env_t结构体头4个bytes是对data的crc校验码,没有定义CONFIG_SYS_REDUNDAND_ENVIRONMENT,所以后面紧跟data数组,数组大小是ENV_SIZE.
ENV_SIZE是CONFIG_ENV_SIZE减掉ENV_HEADER_SIZE,也就是4bytes,
所以env_t这个结构体就包含了整个我们规定的长度为CONFIG_ENV_SIZE的存储区域。
头4bytes是crc校验码,后面剩余的空间全部用来存储环境变量。
需要说明的一点,crc校验码是uboot中在saveenv时计算出来,然后写入nand,所以在第一次启动uboot时crc校验会出错,
因为uboot从nand上读入的一个block数据是随机的,没有意义的,执行saveenv后重启uboot,crc校验就正确了。
data 字段保存实际的环境变量。u-boot的env按name=value”\0”的方式存储,在所有env的最后以”\0\0”表示整个env的结束。
新的name=value对总是被添加到env数据块的末尾,当删除一个name=value对时,后面的环境变量将前移,对一个已经存在的环境变量的修改实际上先删除再插入。 u-boot 把env_t的数据指针还保存在了另外一个地方,这就 是 gd_t结构(不同平台有不同的gd_t结构 ),这里以ARM为例仅列出和env相关的部分
- typedef struct global_data
- {
- …
- unsigned long env_off; /* Relocation Offset */
- unsigned long env_addr; /* Address of Environment struct ??? */
- unsigned long env_valid /* Checksum of Environment valid */
- …
- } gd_t;
typedef struct global_data
{ … unsigned long env_off; /* Relocation Offset */ unsigned long env_addr; /* Address of Environment struct ??? */ unsigned long env_valid /* Checksum of Environment valid */ …
} gd_t;
二 env的初始化
uboot中env的整个架构可以分为3层:
(1) 命令层,如saveenv,setenv editenv这些命令的实现,还有如启动时调用的env_relocate函数。
(2) 中间封装层,利用不同静态存储器特性封装出命令层需要使用的一些通用函数,如env_init,env_relocate_spec,saveenv这些函数。实现文件在common/env_xxx.c
(3) 驱动层,实现不同静态存储器的读写擦等操作,这些是uboot下不同子系统都必须的。
按照执行流顺序,首先分析一下uboot启动的env初始化过程。
首先在board_init_f中调用init_sequence的env_init,这个函数是不同存储器实现的函数,nand中的实现如下:
- <span style="font-size:14px;">/*
- * This is called before nand_init() so we can't read NAND to
- * validate env data.
- *
- * Mark it OK for now. env_relocate() in env_common.c will call our
- * relocate function which does the real validation.
- *
- * When using a NAND boot image (like sequoia_nand), the environment
- * can be embedded or attached to the U-Boot image in NAND flash.
- * This way the SPL loads not only the U-Boot image from NAND but
- * also the environment.
- */
- int env_init(void)
- {
- gd->env_addr = (ulong)&default_environment[0];
- gd->env_valid = 1;
- return 0;
- }</span>
<span style="font-size:14px;">/** This is called before nand_init() so we can't read NAND to* validate env data.** Mark it OK for now. env_relocate() in env_common.c will call our* relocate function which does the real validation.** When using a NAND boot image (like sequoia_nand), the environment* can be embedded or attached to the U-Boot image in NAND flash.* This way the SPL loads not only the U-Boot image from NAND but* also the environment.*/
int env_init(void)
{gd->env_addr = (ulong)&default_environment[0];gd->env_valid = 1;return 0;
}</span>
从注释就基本可以看出这个函数的作用,因为env_init要早于静态存储器的初始化,所以无法进行env的读写,这里将gd中的env相关变量进行配置,
默认设置env为valid。方便后面env_relocate函数进行真正的env从nand到ram的relocate。
继续执行,在board_init_r中,如下:
- /* initialize environment */
- if (should_load_env())
- env_relocate();
- else
- set_default_env(NULL);
/* initialize environment */if (should_load_env())env_relocate();elseset_default_env(NULL);
这是在所有存储器初始化完成后执行的。
首先调用should_load_env,如下:
- /*
- * Tell if it's OK to load the environment early in boot.
- *
- * If CONFIG_OF_CONFIG is defined, we'll check with the FDT to see
- * if this is OK (defaulting to saying it's not OK).
- *
- * NOTE: Loading the environment early can be a bad idea if security is
- * important, since no verification is done on the environment.
- *
- * @return 0 if environment should not be loaded, !=0 if it is ok to load
- */
- static int should_load_env(void)
- {
- #ifdef CONFIG_OF_CONTROL
- return fdtdec_get_config_int(gd->fdt_blob, "load-environment", 1);
- #elif defined CONFIG_DELAY_ENVIRONMENT
- return 0;
- #else
- return 1;
- #endif
- }
/** Tell if it's OK to load the environment early in boot.** If CONFIG_OF_CONFIG is defined, we'll check with the FDT to see* if this is OK (defaulting to saying it's not OK).** NOTE: Loading the environment early can be a bad idea if security is* important, since no verification is done on the environment.** @return 0 if environment should not be loaded, !=0 if it is ok to load*/
static int should_load_env(void)
{
#ifdef CONFIG_OF_CONTROLreturn fdtdec_get_config_int(gd->fdt_blob, "load-environment", 1);
#elif defined CONFIG_DELAY_ENVIRONMENTreturn 0;
#elsereturn 1;
#endif
}
从注释可以看出,CONFIG_OF_CONTROL没有定义,鉴于考虑安全性问题,如果我们想要推迟env的load,可以定义CONFIG_DELAY_ENVIRONMENT,这里返回0,就调用set_default_env使用默认的env,默认env是在配置文件中CONFIG_EXTRA_ENV_SETTINGS设置的。
我们可以在之后的某个地方在调用env_relocate来load env。这里我们选择在这里直接load env。所以没有定义CONFIG_DELAY_ENVIRONMENT,返回1。调用env_relocate。
在common/env_common.c中:
- void env_relocate(void)
- {
- #if defined(CONFIG_NEEDS_MANUAL_RELOC)
- env_reloc();
- env_htab.change_ok += gd->reloc_off;
- #endif
- if (gd->env_valid == 0) {
- #if defined(CONFIG_ENV_IS_NOWHERE) || defined(CONFIG_SPL_BUILD)
- /* Environment not changable */
- set_default_env(NULL);
- #else
- bootstage_error(BOOTSTAGE_ID_NET_CHECKSUM);
- set_default_env("!bad CRC");
- #endif
- } else {
- env_relocate_spec();
- }
- }
void env_relocate(void)
{
#if defined(CONFIG_NEEDS_MANUAL_RELOC)env_reloc();env_htab.change_ok += gd->reloc_off;
#endifif (gd->env_valid == 0) {
#if defined(CONFIG_ENV_IS_NOWHERE) || defined(CONFIG_SPL_BUILD)/* Environment not changable */set_default_env(NULL);
#elsebootstage_error(BOOTSTAGE_ID_NET_CHECKSUM);set_default_env("!bad CRC");
#endif} else {env_relocate_spec();}
}
</pre><p style="font-size: 14px;">Gd->env_valid<span style="font-family: 宋体;">在之前的</span><span style="font-family: Verdana;">env_init</span><span style="font-family: 宋体;">中设置为</span><span style="font-family: Verdana;">1</span><span style="font-family: 宋体;">,所以这里调用</span><span style="font-family: Verdana;">env_relocate_spec</span><span style="font-family: 宋体;">,</span></p><p style="font-size: 14px;"><span style="font-family: 宋体;">这个函数也是不同存储器的中间封装层提供的函数,对于</span><span style="font-family: Verdana;">nand</span><span style="font-family: 宋体;">在</span><span style="font-family: Verdana;">common/env_nand.c</span><span style="font-family: 宋体;">中,如下:</span></p><div class="dp-highlighter bg_cpp"><div class="bar"><div class="tools"><strong>[cpp]</strong> <a target=_blank title="view plain" class="ViewSource" href="http://blog.csdn.net/skyflying2012/article/details/39005705#">view plain</a><a target=_blank title="copy" class="CopyToClipboard" href="http://blog.csdn.net/skyflying2012/article/details/39005705#">copy</a><a target=_blank title="print" class="PrintSource" href="http://blog.csdn.net/skyflying2012/article/details/39005705#">print</a><a target=_blank title="?" class="About" href="http://blog.csdn.net/skyflying2012/article/details/39005705#">?</a><a target=_blank title="在CODE上查看代码片" style="text-indent: 0px;" href="https://code.csdn.net/snippets/462607" target="_blank"><img width="12" height="12" style="left: 2px; top: 1px; position: relative;" alt="在CODE上查看代码片" src="https://code.csdn.net/assets/CODE_ico.png" /></a><a target=_blank title="派生到我的代码片" style="text-indent: 0px;" href="https://code.csdn.net/snippets/462607/fork" target="_blank"><img width="12" height="12" style="left: 2px; top: 2px; position: relative;" alt="派生到我的代码片" src="https://code.csdn.net/assets/ico_fork.svg" /></a></div></div><ol class="dp-cpp"><li class="alt"><span><span class="keyword">void</span><span> env_relocate_spec(</span><span class="keyword">void</span><span>) </span></span></li><li><span>{ </span></li><li class="alt"><span> <span class="datatypes">int</span><span> ret; </span></span></li><li><span> ALLOC_CACHE_ALIGN_BUFFER(<span class="datatypes">char</span><span>, buf, CONFIG_ENV_SIZE); </span></span></li><li class="alt"><span> ret = readenv(CONFIG_ENV_OFFSET, (u_char *)buf); </span></li><li><span> <span class="keyword">if</span><span> (ret) { </span></span></li><li class="alt"><span> set_default_env(<span class="string">"!readenv() failed"</span><span>); </span></span></li><li><span> <span class="keyword">return</span><span>; </span></span></li><li class="alt"><span> } </span></li><li><span> env_import(buf, 1); </span></li><li class="alt"><span>} </span></li></ol></div><pre class="cpp" style="font-size: 14px; display: none;" name="code" code_snippet_id="462607" snippet_file_name="blog_20140902_8_4316261">void env_relocate_spec(void)
{int ret;ALLOC_CACHE_ALIGN_BUFFER(char, buf, CONFIG_ENV_SIZE);ret = readenv(CONFIG_ENV_OFFSET, (u_char *)buf);if (ret) {set_default_env("!readenv() failed");return;}env_import(buf, 1);
}
首先定义一个长度为CONFIG_ENV_SIZE的buf,然后调用readenv,
CONFIG_ENV_OFFSET是在配置文件中定义的env在nand中偏移位置。我们这里定义的是在4M的位置。
Readenv也在env_nand.c中,如下:
- int readenv(size_t offset, u_char *buf)
- {
- size_t end = offset + CONFIG_ENV_RANGE;
- size_t amount_loaded = 0;
- size_t blocksize, len;
- u_char *char_ptr;
- blocksize = nand_info[0].erasesize;
- if (!blocksize)
- return 1;
- len = min(blocksize, CONFIG_ENV_SIZE);
- while (amount_loaded < CONFIG_ENV_SIZE && offset < end) {
- if (nand_block_isbad(&nand_info[0], offset)) {
- offset += blocksize;
- } else {
- char_ptr = &buf[amount_loaded];
- if (nand_read_skip_bad(&nand_info[0], offset,
- &len, NULL,
- nand_info[0].size, char_ptr))
- return 1;
- offset += blocksize;
- amount_loaded += len;
- }
- }
- if (amount_loaded != CONFIG_ENV_SIZE)
- return 1;
- return 0;
- }
int readenv(size_t offset, u_char *buf)
{size_t end = offset + CONFIG_ENV_RANGE;size_t amount_loaded = 0;size_t blocksize, len;u_char *char_ptr;blocksize = nand_info[0].erasesize;if (!blocksize)return 1;len = min(blocksize, CONFIG_ENV_SIZE);while (amount_loaded < CONFIG_ENV_SIZE && offset < end) {if (nand_block_isbad(&nand_info[0], offset)) {offset += blocksize;} else {char_ptr = &buf[amount_loaded];if (nand_read_skip_bad(&nand_info[0], offset,&len, NULL,nand_info[0].size, char_ptr))return 1;offset += blocksize;amount_loaded += len;}}if (amount_loaded != CONFIG_ENV_SIZE)return 1;return 0;
}
Readenv函数利用nand_info[0]对nand进行读操作,读出指定位置,指定长度的数据到buf中。Nand_info[0]是一个全局变量,来表征第一个nand device,这里在nand_init时会初始化这个变量。Nand_init必须在env_relocate之前。
回到env_relocate_spec中,buf读回后调用env_import,如下:
- /*
- * Check if CRC is valid and (if yes) import the environment.
- * Note that "buf" may or may not be aligned.
- */
- int env_import(const char *buf, int check)
- {
- env_t *ep = (env_t *)buf;
- if (check) {
- uint32_t crc;
- memcpy(&crc, &ep->crc, sizeof(crc));
- if (crc32(0, ep->data, ENV_SIZE) != crc) {
- set_default_env("!bad CRC");
- return 0;
- }
- }
- if (himport_r(&env_htab, (char *)ep->data, ENV_SIZE, '\0', 0,
- 0, NULL)) {
- gd->flags |= GD_FLG_ENV_READY;
- return 1;
- }
- error("Cannot import environment: errno = %d\n", errno);
- set_default_env("!import failed");
- return 0;
- }
/** Check if CRC is valid and (if yes) import the environment.* Note that "buf" may or may not be aligned.*/
int env_import(const char *buf, int check)
{env_t *ep = (env_t *)buf;if (check) {uint32_t crc;memcpy(&crc, &ep->crc, sizeof(crc));if (crc32(0, ep->data, ENV_SIZE) != crc) {set_default_env("!bad CRC");return 0;}}if (himport_r(&env_htab, (char *)ep->data, ENV_SIZE, '\0', 0,0, NULL)) {gd->flags |= GD_FLG_ENV_READY;return 1;}error("Cannot import environment: errno = %d\n", errno);set_default_env("!import failed");return 0;
}
首先将buf强制转换为env_t类型,然后对data进行crc校验,跟buf中原有的crc对比,不一致则使用默认env。
最后调用himport_r,该函数将给出的data按照‘\0’分割填入env_htab的哈希表中。
之后对于env的操作,如printenv setenv editenv,都是对该哈希表的操作。
Env_relocate执行完成,env的初始化就完成了。
三 env的操作实现
Uboot对env的操作命令实现在common/cmd_nvedit.c中。
对于setenv printenv editenv这3个命令,看其实现代码,都是对relocate到RAM中的env_htab的操作,这里就不再详细分析了,重点来看一下savenv实现。
- static int do_env_save(cmd_tbl_t *cmdtp, int flag, int argc,
- char * const argv[])
- {
- printf("Saving Environment to %s...\n", env_name_spec);
- return saveenv() ? 1 : 0;
- }
- U_BOOT_CMD(
- saveenv, 1, 0, do_env_save,
- "save environment variables to persistent storage",
- ""
- );
static int do_env_save(cmd_tbl_t *cmdtp, int flag, int argc,char * const argv[])
{printf("Saving Environment to %s...\n", env_name_spec);return saveenv() ? 1 : 0;
}U_BOOT_CMD(saveenv, 1, 0, do_env_save,"save environment variables to persistent storage",""
);
在do_env_save调用saveenv,这个函数是不同存储器实现的封装层函数。对于nand,在common/env_nand.c中,如下:
- int saveenv(void)
- {
- int ret = 0;
- ALLOC_CACHE_ALIGN_BUFFER(env_t, env_new, 1);
- ssize_t len;
- char *res;
- int env_idx = 0;
- static const struct env_location location[] = {
- {
- .name = "NAND",
- .erase_opts = {
- .length = CONFIG_ENV_RANGE,
- .offset = CONFIG_ENV_OFFSET,
- },
- },
- #ifdef CONFIG_ENV_OFFSET_REDUND
- {
- .name = "redundant NAND",
- .erase_opts = {
- .length = CONFIG_ENV_RANGE,
- .offset = CONFIG_ENV_OFFSET_REDUND,
- },
- },
- #endif
- };
- if (CONFIG_ENV_RANGE < CONFIG_ENV_SIZE)
- return 1;
- res = (char *)&env_new->data;
- len = hexport_r(&env_htab, '\0', 0, &res, ENV_SIZE, 0, NULL);
- if (len < 0) {
- error("Cannot export environment: errno = %d\n", errno);
- return 1;
- }
- env_new->crc = crc32(0, env_new->data, ENV_SIZE);
- #ifdef CONFIG_ENV_OFFSET_REDUND
- env_new->flags = ++env_flags; /* increase the serial */
- env_idx = (gd->env_valid == 1);
- #endif
- ret = erase_and_write_env(&location[env_idx], (u_char *)env_new);
- #ifdef CONFIG_ENV_OFFSET_REDUND
- if (!ret) {
- /* preset other copy for next write */
- gd->env_valid = gd->env_valid == 2 ? 1 : 2;
- return ret;
- }
- env_idx = (env_idx + 1) & 1;
- ret = erase_and_write_env(&location[env_idx], (u_char *)env_new);
- if (!ret)
- printf("Warning: primary env write failed,"
- " redundancy is lost!\n");
- #endif
- return ret;
- }
int saveenv(void)
{int ret = 0;ALLOC_CACHE_ALIGN_BUFFER(env_t, env_new, 1);ssize_t len;char *res;int env_idx = 0;static const struct env_location location[] = {{.name = "NAND",.erase_opts = {.length = CONFIG_ENV_RANGE,.offset = CONFIG_ENV_OFFSET,},},
#ifdef CONFIG_ENV_OFFSET_REDUND{.name = "redundant NAND",.erase_opts = {.length = CONFIG_ENV_RANGE,.offset = CONFIG_ENV_OFFSET_REDUND,},},
#endif};if (CONFIG_ENV_RANGE < CONFIG_ENV_SIZE)return 1;res = (char *)&env_new->data;len = hexport_r(&env_htab, '\0', 0, &res, ENV_SIZE, 0, NULL);if (len < 0) {error("Cannot export environment: errno = %d\n", errno);return 1;}env_new->crc = crc32(0, env_new->data, ENV_SIZE);
#ifdef CONFIG_ENV_OFFSET_REDUNDenv_new->flags = ++env_flags; /* increase the serial */env_idx = (gd->env_valid == 1);
#endifret = erase_and_write_env(&location[env_idx], (u_char *)env_new);
#ifdef CONFIG_ENV_OFFSET_REDUNDif (!ret) {/* preset other copy for next write */gd->env_valid = gd->env_valid == 2 ? 1 : 2;return ret;}env_idx = (env_idx + 1) & 1;ret = erase_and_write_env(&location[env_idx], (u_char *)env_new);if (!ret)printf("Warning: primary env write failed,"" redundancy is lost!\n");
#endifreturn ret;
}
定义env_t类型的变量env_new,准备来存储env。
利用函数hexport_r对env_htab操作,读取env内容到env_new->data,
校验data,获取校验码env_new->crc。
最后调用erase_and_write_env将env_new先擦后写入由location定义的偏移量和长度的nand区域中。
这样就完成了env写入nand的操作。
在savenv readenv函数以及printenv setenv的实现函数中涉及到的函数himport_r hexport_r hdelete_r hmatch_r都是对env_htab哈希表的一些基本操作函数。
这些函数都封装在uboot的lib/hashtable.c中,这里就不仔细分析这些函数了。
uboot环境变量实现分析相关推荐
- uboot环境变量及常用命令
U-Boot环境变量的解释说明 环 境 变 量 解 释 说 明 bootdelay 定义执行自动启动的等候秒数 baudrate 定义串口控制台的波特率 netmask 定义以太网接口的掩码 etha ...
- 实战-Android 系统读取Uboot环境变量实现
需求 系统起来之后,很多时候需要读取或者设置Uboot下的环境变量 实现 不同平台读取uboot环境变量略微有差异,思路都大同小异.Uboot底层有读取环境变量的实现,可参考实现. 这里以Amlogi ...
- uboot 修改linux密码,Linux下设置u-boot环境变量----fw_setenv
打印uboot环境变量: fw_printenv [[ -n name ] | [ name ... ]] # ./fw_printenv -n baudrate 如果不指定name,fw_print ...
- uboot环境变量setenv学习
setenv是操作uboot中环境变量的一个命令 命令操作的简单回顾: 新建一个环境变量 命令行:set newenv 1 结果: newenv=1 删掉一个环境变量 命令行:set newenv 结 ...
- u-boot环境变量设置
这是我uboot的环境变量设置,在该设置下可以运行initram内核(从内存下载到nandflash再运行),但是运行nfs根文件系统的时候一直出错,各种错误.查看了很多资料后猜想应该是uboot传递 ...
- uboot环境变量与内核MTD分区关系
uboot 与系统内核中MTD分区的关系: 分区只是内核的概念,就是说A-B地址放内核,C-D地址放文件系统,(也就是规定哪个地址区间放内核或者文件系统)等等. 1:在内核MTD中可以定义分区A~B, ...
- uboot环境变量(设置bootargs向linux内核传递正确的参数)
http://blog.csdn.net/workhorse/article/details/7071428 这是我uboot的环境变量设置,在该设置下可以运行initram内核(从内存下载到nand ...
- HI3516DV300笔记(四)修改uboot环境变量
开发板烧写uboot后,会修改环境变量到默认配置,可以修改配置文件,修改默认的环境变量. 修改u-boot-2016.11/include/configs/hi3516dv300.h /* Assum ...
- uboot环境变量保存到EMMC
1. cmd 命令行可以用setenv printenv saveenv uboot\u-boot-2020.04\cmd\nvedit.c setenv -> do_env_set -> ...
最新文章
- vb初学者编程代码大全_学习VB编程第14天,一个简单的排序代码把我难住了
- SAP 应用服务负载均衡的实现
- leetcode17 电话号码的字母组合
- 新一代Hologres实时数仓大揭秘
- 数字图像处理之点运算
- 从源代码角度看Struts2返回JSON数据的原理
- Android Studio (15)---界面原型设计
- HTML5新API记录
- linux如何把文件导出到ftp,Linux如何利用ssh传送文件至FTP空间
- Power up! 这三个小时IBM都讲了些啥?
- 移动开发者如何获取免费流量
- 快手火山抖音视频(包含其他视频)跨平台操作搬运,下载,消重,全自动操作解放双手...
- 音视频进阶教程|实现直播SEI补充增强信息
- Windows11不显示WiFi图标
- 手把手Selenium安装使用及实战爬取前程无忧招聘网站(一)
- Web页面快捷键自定义
- 【报告分享】元宇宙发展研究报告2.0版-清华大学(附下载)
- Java安装开发环境
- 【Unity3d基础】Unity换装系统(1)
- KOA中await next分析?