操作系统学习:Linux0.12初始化详细流程-打开文件与加载可执行程序
本文参考书籍
1.操作系统真相还原
2.Linux内核完全剖析:基于0.12内核
3.x86汇编语言 从实模式到保护模式
4.Linux内核设计的艺术
ps:基于x86硬件的pc系统
Linux0.12初始化续
本次主要分析文件的打开与可执行程序的加载。
打开文件与中断的执行过程
上文分析完成了setup函数的执行,此时返回init函数继续执行,此时会执行到;
(void) open("/dev/tty1",O_RDWR,0); // 以读写方式打开tty
我们查看open函数的定义,位于lib/open.c中;
int open(const char * filename, int flag, ...) // 打开文件函数 filename文件名,flag文件打开标志
{ // 打开文件返回文件描述符register int res;va_list arg;va_start(arg,flag); // 输入的可变参数__asm__("int $0x80":"=a" (res):"0" (__NR_open),"b" (filename),"c" (flag),"d" (va_arg(arg,int))); // 将参数传入并调用读文件的系统调用if (res>=0) // 返回文件大于等于0 则表示是一个文件描述符return res; // 返回打开的文件描述符errno = -res;return -1;
}
此时进行系统调用后,会在内核态下执行sys_open函数;
int sys_open(const char * filename,int flag,int mode) // 打开文件系统调用
{struct m_inode * inode;struct file * f;int i,fd;mode &= 0777 & ~current->umask; // 将用户设置的模式与当前进程屏蔽码相与for(fd=0 ; fd<NR_OPEN ; fd++) // 从20中查找一个当前进程没有使用的fdif (!current->filp[fd])break;if (fd>=NR_OPEN) // 如果打开文件的个数超过20则返回出错码return -EINVAL;current->close_on_exec &= ~(1<<fd); // 设置当前进程的执行时关闭文件句柄位图f=0+file_table; // 获取file_table数组的首地址,赋值给ffor (i=0 ; i<NR_FILE ; i++,f++) // 搜索空闲文件结构项if (!f->f_count) break; // 如果找到空闲则停止循环if (i>=NR_FILE) // 如果超过64个,即没有找到空闲的则返回出错码return -EINVAL;(current->filp[fd]=f)->f_count++; // 如果找到则将当前进程对应的文件句柄fd指向搜索到的文件结构if ((i=open_namei(filename,flag,mode,&inode))<0) { // 执行打开操作,如果返回小于0说明出错current->filp[fd]=NULL; // 释放刚刚找到的结构f->f_count=0; // 将找到的文件结构引用计数置空return i; // 返回出错码}
/* ttys are somewhat special (ttyxx major==4, tty major==5) */if (S_ISCHR(inode->i_mode)) // 检查是否是字符设备if (check_char_dev(inode,inode->i_zone[0],flag)) { // 检查是否能打开该字符设备iput(inode); // 如果不能打开则释放inode并重新置为返回出错码current->filp[fd]=NULL;f->f_count=0;return -EAGAIN;}
/* Likewise with block-devices: check for floppy_change */if (S_ISBLK(inode->i_mode)) // 如果打开的是块设备check_disk_change(inode->i_zone[0]); // 检查盘片是否更换f->f_mode = inode->i_mode; // 设置文件属性f->f_flags = flag; // 设置文件标志f->f_count = 1; // 设置文件句柄引用计数为1f->f_inode = inode; // 设置i节点为打开文件的i节点f->f_pos = 0; // 初始化文件读写指针return (fd); // 返回文件句柄
}
读函数,主要是找到当前进程读写的空闲的文件描述符,然后将数据放入inode中,然后让打开的文件结构指向打开的i节点,在本函数中比较核心的函数是open_namei;
int open_namei(const char * pathname, int flag, int mode,struct m_inode ** res_inode)
{const char * basename;int inr,dev,namelen;struct m_inode * dir, *inode;struct buffer_head * bh;struct dir_entry * de;if ((flag & O_TRUNC) && !(flag & O_ACCMODE)) // 如果文件访问模式是只读但是截位标志置位了添加只写标志,flag |= O_WRONLY;mode &= 0777 & ~current->umask; // 屏蔽给定模式的位mode |= I_REGULAR; // 并添加上普通文件标志位if (!(dir = dir_namei(pathname,&namelen,&basename,NULL))) // 根据指定的路径名寻找对应的i节点return -ENOENT;if (!namelen) { /* special case: '/usr/' etc */ // 如果长度为0if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) { // 如果不是读写、创建和文件长度截0*res_inode=dir; // 则是打开一个目录名文件操作return 0; // 直接赋值返回}iput(dir); // 否则释放该节点return -EISDIR; // 返回错误码}bh = find_entry(&dir,basename,namelen,&de); // 根据最顶层目录名的iJ节点dir,在取得路径名中最后的文件名对应的目录项结构de,并获取该目录的缓冲区指针if (!bh) { // 如果返回为空if (!(flag & O_CREAT)) { // 如果不是创建文件则释放返回iput(dir);return -ENOENT;}if (!permission(dir,MAY_WRITE)) { // 如果该用户在目录下没有写的权利则释放并返回错误码iput(dir);return -EACCES;}inode = new_inode(dir->i_dev); // 是创建操作并且有写操作权限,申请该目录下一个i节点if (!inode) { // 如果申请失败则释放并返回错误码iput(dir);return -ENOSPC;}inode->i_uid = current->euid; // 设置节点的用户idinode->i_mode = mode; // 设置节点的模式inode->i_dirt = 1; // 设置修改标志bh = add_entry(dir,basename,namelen,&de); // 添加指定目录添加一个新目录项if (!bh) { // 如果添加失败inode->i_nlinks--; // 节点的引用连接计数减1iput(inode); // 释放该节点iput(dir); // 释放申请的目录return -ENOSPC; // 返回出错码}de->inode = inode->i_num; // 置i节点号为新申请的iJ节点号码bh->b_dirt = 1; // 置高速缓冲区已修改标志brelse(bh); // 释放缓冲块iput(dir); // 放回目录9节点*res_inode = inode; // 返回新目录项的i节点指针return 0;}inr = de->inode; // 若去文件名对应的目录项结构成功则该文件存在,取该节点的节点号dev = dir->i_dev; // 取所在设备的设备号brelse(bh); // 释放缓冲块if (flag & O_EXCL) { // 如果此时独占操作标志置位,但文件已经存在则返回文件出错码iput(dir); return -EEXIST;}if (!(inode = follow_link(dir,iget(dev,inr)))) // 读取该目录项的i节点内容return -EACCES;if ((S_ISDIR(inode->i_mode) && (flag & O_ACCMODE)) ||!permission(inode,ACC_MODE(flag))) { // 如果该i节点是一个目录的i节点并且访问模式是只写或读写或者没有访问权限则放回该节点并返回错误码iput(inode);return -EPERM;}inode->i_atime = CURRENT_TIME; // 更新该i节点的访问时间if (flag & O_TRUNC) // 如果设立了截0标志truncate(inode); // 则将该i节点的文件长度截为0并返回目录项i节点的指针*res_inode = inode;return 0;
}
该函数相对比较复杂,我们依次来分析所调用的函数dir_namei,该函数主要是找到指定目录名的i节点指针,以及在最顶层目录的名称,其中涉及的调用函数分析如下;
static struct m_inode * get_dir(const char * pathname, struct m_inode * inode)
{char c;const char * thisname;struct buffer_head * bh;int namelen,inr;struct dir_entry * de;struct m_inode * dir;if (!inode) { // 检查传入inode是否为空inode = current->pwd; // 如果为空则使用当前进程的当前工作目录inode->i_count++; // 当前inode引用计数加1}if ((c=get_fs_byte(pathname))=='/') { // 如果用户指定路径的第一个字符是/,则是绝对路径iput(inode); // 放回原节点inode = current->root; // 从当前任务的根节点开始查找pathname++; // 路径名指针向下移动一位inode->i_count++; // inode引用计数加1}while (1) { // 路径名中各个目录名部分和文件名进行循环处理thisname = pathname; // 当前路径名if (!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) {iput(inode); // 如果当前节点不是目录名或者没有进入该目录的权限放回并返回return NULL; }for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)/* nothing */ ; // 找到下一个/之前的名称if (!c) // 如果c为空则直接返回该inodereturn inode;if (!(bh = find_entry(&inode,thisname,namelen,&de))) {iput(inode); // 找到当前目录项后然后查找该目录项inode如果没找到则放回并返回return NULL;}inr = de->inode; // 获取该节点的节点号brelse(bh); // 释放缓冲块dir = inode; if (!(inode = iget(dir->i_dev,inr))) { // 取节点的内容iput(dir);return NULL;}if (!(inode = follow_link(dir,inode))) // 如果当前目录是一个符号链接则取符号链接指向的i节点return NULL;}
}/** dir_namei()** dir_namei() returns the inode of the directory of the* specified name, and the name within that directory.*/
static struct m_inode * dir_namei(const char * pathname,int * namelen, const char ** name, struct m_inode * base) // 返回指定目录名的i节点指针,以及在最顶层目录的名称
{char c;const char * basename;struct m_inode * dir;if (!(dir = get_dir(pathname,base))) // base是指定的起始目录i节点return NULL; // 如果没找到则返回为空basename = pathname; // while (c=get_fs_byte(pathname++)) // if (c=='/')basename=pathname;*namelen = pathname-basename-1; // 返回名长度*name = basename; // 返回名称return dir;
}
此时我们继续分析find_entry函数,该函数主要是查找指定目录和文件名的目录项;
static struct buffer_head * find_entry(struct m_inode ** dir,const char * name, int namelen, struct dir_entry ** res_dir)
{int entries;int block,i;struct buffer_head * bh;struct dir_entry * de;struct super_block * sb;#ifdef NO_TRUNCATE // 如果定义了NO_TRUNCATEif (namelen > NAME_LEN) // 如果文件名长度超过最大长度则返回return NULL;
#elseif (namelen > NAME_LEN) // 如果文件名长度超过定义则截取NAME_LEN长度的名字namelen = NAME_LEN;
#endifentries = (*dir)->i_size / (sizeof (struct dir_entry)); // 计算目录中目录项项数*res_dir = NULL;
/* check for '..', as we might have to do some "magic" for it */if (namelen==2 && get_fs_byte(name)=='.' && get_fs_byte(name+1)=='.') { // 如果是 . 和 .. 这种情况
/* '..' in a pseudo-root results in a faked '.' (just change namelen) */ if ((*dir) == current->root) // 如果是 .. 则转换为 .namelen=1;else if ((*dir)->i_num == ROOT_INO) { // 如果该目录节点号为1
/* '..' over a mount-point results in 'dir' being exchanged for the mounteddirectory-inode. NOTE! We set mounted, so that we can iput the new dir */sb=get_super((*dir)->i_dev); // 读取文件系统的根i节点if (sb->s_imount) { // 如果已经安装了根节点iput(*dir); // 则放回dir及节点(*dir)=sb->s_imount; // 将安装的根节点指向dir(*dir)->i_count++; // 引用计数加1}}}if (!(block = (*dir)->i_zone[0])) // 如果0不包含数据则返回return NULL;if (!(bh = bread((*dir)->i_dev,block))) // 读取节点0节点的数据return NULL;i = 0;de = (struct dir_entry *) bh->b_data; // 获取读取的数据让de指向该数据while (i < entries) {if ((char *)de >= BLOCK_SIZE+bh->b_data) { // 如果搜索完成还没找到brelse(bh); // 释放该缓冲块bh = NULL; // 指针置空if (!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) ||!(bh = bread((*dir)->i_dev,block))) { // 通过读取目录的下一个逻辑块i += DIR_ENTRIES_PER_BLOCK; // 继续寻找continue;}de = (struct dir_entry *) bh->b_data;}if (match(namelen,name,de)) { // 判断是否匹配目录*res_dir = de; // 如果匹配上则让res_dir指向找到的dereturn bh; // 返回缓冲块}de++; // 否则进行下一次寻找i++;}brelse(bh); // 如果最终没找到则释放该缓冲块return NULL; // 返回为空
}
继续执行时,由于此时传入的参数为’/dev/tty1’,打开的是终端设备,此时会执行check_char_dev函数,该函数主要是检查如果打开的文件是tty终端字符设备时,对当前进程的设置和tty表的设置。
至此open的系统调用就完成了。
输入输出重定向
(void) dup(0); // 复制句柄,产生句柄1号标准输出设备(void) dup(0);
此时也会使用系统调用,此时调用sys_dup()函数;
int sys_dup(unsigned int fildes)
{return dupfd(fildes,0);
}
dupfd函数如下;
static int dupfd(unsigned int fd, unsigned int arg) // 复制文件句柄函数
{if (fd >= NR_OPEN || !current->filp[fd]) // 检查函数参数的有效性,如果大于20或者该文件描述符对应的文件结构为空return -EBADF; // 返回错误码if (arg >= NR_OPEN) // 如果输入的参数小于20则返回错误码return -EINVAL;while (arg < NR_OPEN) // 循环遍历,找到一个可用的文件描述符 if (current->filp[arg]) arg++;elsebreak;if (arg >= NR_OPEN) // 如果找到的文件描述符大于20,则没有找到空闲的则返回错误码return -EMFILE;current->close_on_exec &= ~(1<<arg); // 找到空闲后关闭标志,即在exec的过程中不会关闭dup的文件描述符(current->filp[arg] = current->filp[fd])->f_count++; // 将文件引用计数增1return arg; // 返回文件描述符
}
该函数主要是复制文件句柄。
可执行程序的加载
在初始化程序中继续执行,此时会生成子进程,子进程中执行,将/etc/rc文件中的内容输出到终端上,然后调用execve执行/etc/rc文件中的命令。
if (!(pid=fork())) { // 执行/etc/rc中的命令参数 close(0);if (open("/etc/rc",O_RDONLY,0)) // 将打开文件重定向到标准输入_exit(1); // 若文件打开失败则立刻退出execve("/bin/sh",argv_rc,envp_rc); // 系统调用执行命令_exit(2); // 若执行出错则退出}
此时我们来分析一下execve函数的执行过程,该函数也是通过系统调用调用sys_execve函数;
_sys_execve:lea EIP(%esp),%eax # eax指向堆栈中保存用户程序的eippushl %eax # 将eax压栈call _do_execve # 调用c函数do_execve函数addl $4,%esp # 丢弃压入的值ret
此时我们继续查看do_execve函数;
int do_execve(unsigned long * eip,long tmp,char * filename,char ** argv, char ** envp)
{struct m_inode * inode;struct buffer_head * bh;struct exec ex;unsigned long page[MAX_ARG_PAGES]; // 参数和环境空间页面指针数组int i,argc,envc;int e_uid, e_gid; // 有效用户ID和有效组IDint retval;int sh_bang = 0; // 控制是否需要执行脚本程序unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4; // p指向参数和环境的最后部分if ((0xffff & eip[1]) != 0x000f) // 段选择符是否是当前任务代码段的段选择符panic("execve called from supervisor mode");for (i=0 ; i<MAX_ARG_PAGES ; i++) /* clear page-table */ // 清空Page数组page[i]=0;if (!(inode=namei(filename))) /* get executables inode */ // 获取文件名对应的inode节点return -ENOENT; argc = count(argv); // 命令行参数个数envc = count(envp); // 环境字符串变量个数restart_interp:if (!S_ISREG(inode->i_mode)) { /* must be regular file */ // 是否是常规文件retval = -EACCES;goto exec_error2;}i = inode->i_mode; // 获取i节点的属性e_uid = (i & S_ISUID) ? inode->i_uid : current->euid; // 判断进程是否有运行的权利e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;if (current->euid == inode->i_uid) // 如果当前进程的euid与节点的uid相同i >>= 6; // 则文件属性值右移6位else if (in_group_p(inode->i_gid)) // 如果是同组用户i >>= 3; // 则文件属性值右移3位if (!(i & 1) &&!((inode->i_mode & 0111) && suser())) { // 根据属性i的最低3位来判断当前进程是否有权运行这个文件retval = -ENOEXEC; // 如果不能运行则跳转goto exec_error2;}if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) { // 读取可执行文件的第1块数据到缓冲区retval = -EACCES;goto exec_error2;}ex = *((struct exec *) bh->b_data); /* read exec-header */ // 将读入的数据让ex指向该数据if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) { // 如果是以#!开头则是脚本文件/** This section does the #! interpretation.* Sorta complicated, but hopefully it will work. -TYT*/char buf[128], *cp, *interp, *i_name, *i_arg;unsigned long old_fs;strncpy(buf, bh->b_data+2, 127); // 提取脚本程序名与参数并把解释程序名、解释程序的参数和脚本文件名组合放入环境参数块中brelse(bh); // 释放该缓冲块iput(inode); // 放回脚本文件节点buf[127] = '\0'; if (cp = strchr(buf, '\n')) {*cp = '\0'; // 第1个换行符并去掉空格制表符for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);}if (!cp || *cp == '\0') { // 若改行没内容则报错retval = -ENOEXEC; /* No interpreter name found */goto exec_error1;}interp = i_name = cp; // 得到程序的内容i_arg = 0; // 获取程序的名称和程序执行的输入参数for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {if (*cp == '/')i_name = cp+1;}if (*cp) {*cp++ = '\0';i_arg = cp;}/** OK, we've parsed out the interpreter name and* (optional) argument.*/if (sh_bang++ == 0) { // sh_bang加1p = copy_strings(envc, envp, page, p, 0); // 把函数的参数放入空间中p = copy_strings(--argc, argv+1, page, p, 0); // 除了执行文件名其他都放入空间中}/** Splice in (1) the interpreter's name for argv[0]* (2) (optional) argument to interpreter* (3) filename of shell script** This is done in reverse order, because of how the* user environment and arguments are stored.*/p = copy_strings(1, &filename, page, p, 1); // 接着逆向复制文件名argc++; if (i_arg) { // 复制解释程序的多个参数p = copy_strings(1, &i_arg, page, p, 2);argc++;}p = copy_strings(1, &i_name, page, p, 2); argc++;if (!p) { // 如果复制不成功则返回错误码retval = -ENOMEM;goto exec_error1;}/** OK, now restart the process with the interpreter's inode.*/old_fs = get_fs(); // 让段寄存器指向内核空间set_fs(get_ds()); // 设置段寄存器if (!(inode=namei(interp))) { /* get executables inode */ // 获取可执行i节点set_fs(old_fs); // 如果出错则设置成原值并设置错误码返回retval = -ENOENT;goto exec_error1;}set_fs(old_fs); // 设置成原段寄存器goto restart_interp; // 重新处理新的执行文件}brelse(bh); // 释放缓冲块if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||ex.a_text+ex.a_data+ex.a_bss>0x3000000 || // 此时可执行文件的头机构数据已经复制到了ex中inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) { // 检查头部文件格式如果不对则设置错误码返回retval = -ENOEXEC;goto exec_error2;}if (N_TXTOFF(ex) != BLOCK_SIZE) { // 如果文件代码开始处没有位于1个页面边界处,则不能执行printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename); // 因为需求页需要加载执行文件时以页面为单位retval = -ENOEXEC;goto exec_error2;}if (!sh_bang) { // 如果该标志没有被设置,p = copy_strings(envc,envp,page,p,0); // 复制指定个数的命令行参数和环境字符串到参数和环境空间中p = copy_strings(argc,argv,page,p,0);if (!p) { // 如果此时p为0则表示空间已满retval = -ENOMEM;goto exec_error2;}}
/* OK, This is the point of no return */
/* note that current->library stays unchanged by an exec */if (current->executable) // 如果当前可执行有值则放回对应的inodeiput(current->executable);current->executable = inode; // 让进程executable指向新执行文件的i节点current->signal = 0; // 复位所有的信号位图for (i=0 ; i<32 ; i++) { // 复位原进程的所有信号处理句柄current->sigaction[i].sa_mask = 0;current->sigaction[i].sa_flags = 0;if (current->sigaction[i].sa_handler != SIG_IGN)current->sigaction[i].sa_handler = NULL;}for (i=0 ; i<NR_OPEN ; i++) // 根据设定的执行时关闭位图标志,是否关闭指定的打开文件if ((current->close_on_exec>>i)&1)sys_close(i);current->close_on_exec = 0; // 复位该标志free_page_tables(get_base(current->ldt[1]),get_limit(0x0f)); // 设置当前进程的基地址和限长free_page_tables(get_base(current->ldt[2]),get_limit(0x17));if (last_task_used_math == current) // 是否使用了协处理器last_task_used_math = NULL; // 如果该进程使用了则重置current->used_math = 0; // 复位该标志p += change_ldt(ex.a_text,page); // 根据执行文件头的代码长度字段修改局部表中描述符基址和段限长p -= LIBRARY_SIZE + MAX_ARG_PAGES*PAGE_SIZE; // 并将128KB的参数和环境空间放置在数据段末端p = (unsigned long) create_tables((char *)p,argc,envc); // 在栈空间中创建环境和参数变量指针表current->brk = ex.a_bss +(current->end_data = ex.a_data +(current->end_code = ex.a_text)); // 指向当前任务数据段的末端current->start_stack = p & 0xfffff000; // 进程栈开始字段所在页面current->suid = current->euid = e_uid; // 重新设置有效用户id和有效组idcurrent->sgid = current->egid = e_gid;eip[0] = ex.a_entry; /* eip, magic happens :-) */ // 将系统中断在堆栈上的代码指针换为新执行程序的入口点eip[3] = p; /* stack pointer */ // 将栈指针替换为新执行文件的栈指针return 0;
exec_error2:iput(inode); // 放回i节点
exec_error1:for (i=0 ; i<MAX_ARG_PAGES ; i++) // 释放存放参数的内存页面free_page(page[i]);return(retval); // 返回出错码
}
该函数主要的工作是,执行对命令行参数和环境参数空间页面的初始化操作,根据执行文件开始部分的头结构、对其中信息进行处理,对当前调用进程运行新文件前进行初始化操作,替换堆栈上原调用程序的返回地址为新执行程序运行地址,运行新加载的程序。
至此文件打开与可执行程序的加载分析流程已完成。
操作系统学习:Linux0.12初始化详细流程-打开文件与加载可执行程序相关推荐
- 操作系统学习:Linux0.12初始化详细流程-进程1加载虚拟盘和根文件系统安装
本文参考书籍 1.操作系统真相还原 2.Linux内核完全剖析:基于0.12内核 3.x86汇编语言 从实模式到保护模式 4.Linux内核设计的艺术 ps:基于x86硬件的pc系统 Linux0.1 ...
- 操作系统学习:Linux0.12初始化详细流程-进程1调度与读取硬盘数据
本文参考书籍 1.操作系统真相还原 2.Linux内核完全剖析:基于0.12内核 3.x86汇编语言 从实模式到保护模式 4.Linux内核设计的艺术 ps:基于x86硬件的pc系统 Linux0.1 ...
- 操作系统学习:Linux0.12初始化详细流程-首个子进程
本文参考书籍 1.操作系统真相还原 2.Linux内核完全剖析:基于0.12内核 3.x86汇编语言 从实模式到保护模式 4.Linux内核设计的艺术 ps:基于x86硬件的pc系统 Linux0.1 ...
- 操作系统学习:Linux0.12初始化详细流程-进程退出与系统进入怠速
本文参考书籍 1.操作系统真相还原 2.Linux内核完全剖析:基于0.12内核 3.x86汇编语言 从实模式到保护模式 4.Linux内核设计的艺术 ps:基于x86硬件的pc系统 Linux0.1 ...
- 操作系统学习:系统调用与Linux0.12初始化详细流程
本文参考书籍 1.操作系统真相还原 2.Linux内核完全剖析:基于0.12内核 3.x86汇编语言 从实模式到保护模式 4.Linux内核设计的艺术 ps:基于x86硬件的pc系统 系统调用 系统调 ...
- 【OS学习笔记】十 实模式:实现一个程序加载器-程序加载器如何将用户程序加载到内存并执行
上一篇文章学习了以下内容: 用一种不同的分段方法,从另一个不同的的角度理解处理器的分段内存访问机制 使用循环和条件转移指令来优化主引导扇区代码 点击链接查看上一篇文章:点击链接查看 对于主引导扇区部分 ...
- java web配置dll文件_JavaWeb项目中dll文件动态加载方法解析(详细步骤)
相信很多做Java的朋友都有过用Java调用JNI实现调用C或C++方法的经历,那么Java Web中又如何实现DLL/SO文件的动态加载方法呢.今天就给大家带来一篇JAVA Web项目中DLL/SO ...
- linux so lazyload,linux函数深入探索——open函数打开文件是否将文件内容加载到内存空间...
转自:https://blog.csdn.net/qq_17019203/article/details/85051627 问题:open(2)函数打开文件是否将文件内容加载到内存空间 首先,文件打开 ...
- Qt实用技巧:ubuntu发布程序打包流程(解决插件xcb加载失败)
若该文为原创文章,未经允许不得转载 原博主博客地址:长沙红胖子Qt的博客_CSDN博客-Qt开发,图形图像处理,OpenCV图像处理领域博主 原博主博客导航:红胖子网络科技博文大全:开发技术集合(包含 ...
最新文章
- ROS与深度相机入门教程:(2) 在ROS中驱动Intel D435i深度相机采集数据(遇到的问题)
- vs显示不是有效的window32_玩转“黑科技”,这才是选购“户外显示屏”的正确方式!...
- stack与queue
- Spring AOP详解(http://sishuok.com/forum/posts/list/281.html)
- MQTT在游戏运营发行中的实践
- Qt文档阅读笔记-编写应用脚本解析与实例
- 孩子要经历什么后,才能懂得学业的重要和父母的不易?
- 一道曾经微软的面试题
- 多旋翼智能飞行和视觉识别(H题)(组委会自命题)
- 2022年信息安全工程师考试知识点:访问控制
- echar生成雷达图
- 基于STM32和阿里云的智能家居
- reversed python_python字典reversed
- 用vs打开已有web项目运行时显示网页无法访问
- 华为云王红新_华为云新加坡峰会盛大举行,多家公司签署MoU
- Linux性能分析工具总结
- python安装xgboost的方法
- 流媒体服务器(1)—— 一个非常好用云转码流媒体平台
- 两部苹果手机同步照片_苹果手机如何恢复照片?最近删除清空也不用怕!
- 【正一专栏】战长沙——血性尊严
热门文章
- 顺络新能源汽车技术研讨会圆满落幕
- ​《头号玩家》中的“绿洲”,用 VR 可以找到
- 韩辉:国产操作系统的最大难题在于解决“生产关系”
- 漫画 | 程序媛小姐姐带你一次了解什么是排序算法
- DeeCamp 2020 赛题大公开!快来看你想选哪个
- 让AI训练AI,阿里和浙大的“AI训练师助手”是这样炼成的
- 七夕大礼包:26个AI学习资源送给你!
- 算法 | 动画+解析,轻松理解「Trie树」
- Python需求增速达174%,AI人才缺口仍超百万!这份来自2017年的实际招聘数据如是说
- JDK 8 Stream 数据流效率怎么样?