作者:刘长元
链接:https://www.zhihu.com/question/430325644/answer/1575817073
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本回答基本上是从下面这篇文章翻译而来,但是没有完全照抄,而是加了一些相关资料和我的想法。我也不搬英文原文过来了,有兴趣的可以直接点进去看。

https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-4.html​0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-4.html

一、 Bash启动阶段

  1. 我们设定前提,CentOS 7.0版本,无桌面环境,Bash版本为4.2.x
  2. 当系统启动后,我们在系统的终端直接输入用户名密码登录,或者通过SSH方式远程登录,都会启动登录用户配置的Shell,我们这里假定为Bash。
  3. Bash的源码被放在Github中,我们可以查看,地址为(bminor/bash),我们接下来看的代码会是bash-4.2的Tag的版本。
  4. Bash自身是通过C语言编写,入口方法为main,Bash的main方法的代码可以在这里看到,https://github.com/bminor/bash/blob/bash-4.2/shell.c#L342,它的代码比较长,总结下来是做了下面几件事
    1. 尝试打开/dev/tty
    2. 检查Shell是否运行在Debug模式
    3. 设定默认语言
    4. 设定uid
    5. 分析命令行参数
    6. 读取Shell环境变量
    7. 加载.bashrc, .profile 和其它配置文件
    8. 其它很多很多
  5. 在上述步骤完成以后,就会调用reader_loop方法,从名字上就能看出来,这个方法会不断重复的读取用户输入,也就是我们平时在使用Bash时的交互,你输入指令,Bash去执行
  6. reader_loop方法的代码在这里(https://github.com/bminor/bash/blob/bash-4.2/eval.c#L63:1),这个方法里有一个While方法,它会一直不断等待你的输入,执行你的指令。

二、 Bash接收并执行指令阶段

  1. 在eval.c的reader_loop方法中,通过while方法,得到你的指令输入,然后在做完一系列的给定程序名称和参数检查后,它会调用execute_cmd.c中的execute_command方法 。代码在这里(https://github.com/bminor/bash/blob/bash-4.2/execute_cmd.c#L362)
  2. execute_command方法会调用本文件中的execute_command_internal方法,以同步方式执行指令
  3. execute_command_internal方法在做了一堆检查和准备工作后,调用了execute_simple_command方法。
  4. 在这个方法中,Bash会通过find_special_builtinfind_function方法来确认你的指令是否为内建指令或已经声明的方法,如果是则会直接去执行,否则继续调用execute_disk_command方法来执行保存在磁盘中的命令。
  5. execute_disk_command方法中,Bash会在目录中查找你给定的程序文件,这和你输入的命令是否为绝对路径,PATH环境变量相关。然后执行shell_execve 方法。
  6. shell_execve方法进来后就会直接执行系统调用(SYSCALL)execve(command, args, env),从这里为分界点,前面是Bash的事情,后面就是Linux Kernel里的东西了。
  7. ------------小PS分割线-------------
  8. ps1: 有兴趣的话,github.com/bminor/bash/blob/bash-4.2/execute_cmd.c 文件里的这几个方法可以看看,还是挺好玩的。
  9. ps2: 我们可以通过strace命令来测试,执行ls命令时,第一条syscall就是execve
[root@k8s ~]# strace ls 2>&1|more
execve("/usr/bin/ls", ["ls"], 0x7ffd94626050 /* 24 vars */) = 0
brk(NULL)                               = 0x559a9f5db000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffc92aad630) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)[root@k8s ~]# strace echo
execve("/usr/bin/echo", ["echo"], 0x7ffee01f5530 /* 24 vars */) = 0
brk(NULL)                               = 0x559f158bf000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffcca88f210) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

三、系统调用execve 阶段

  • 进入到这步后,要看的源码就是Linux Kernel了,地址是 github.com/torvalds/linux ,本次看的版本是CentOS 7使用的3.10
  • execve的系统调用定义在这里,https://github.com/torvalds/linux/blob/v3.10/fs/exec.c#L1677,它直接把接收来的文件名称,参数和环境变量,转手传给了do_execve方法
 SYSCALL_DEFINE3(execve,const char __user *, filename,const char __user *const __user *, argv,const char __user *const __user *, envp)
{struct filename *path = getname(filename);int error = PTR_ERR(path);if (!IS_ERR(path)) {error = do_execve(path->name, argv, envp);putname(path);}return error;
}
  • do_execve方法也很简单,定义了argv和envp指针以后,调用do_execve_common方法
int do_execve(const char *filename,const char __user *const __user *__argv,const char __user *const __user *__envp)
{struct user_arg_ptr argv = { .ptr.native = __argv };struct user_arg_ptr envp = { .ptr.native = __envp };return do_execve_common(filename, argv, envp);
}
  • do_execve_common方法做了一堆进程执行的准备工作
## 1. 检查Limits,进程数是否已经超出/** We move the actual failure in case of RLIMIT_NPROC excess from* set*uid() to execve() because too many poorly written programs* don't check setuid() return code.  Here we additionally recheck* whether NPROC limit is still exceeded.*/if ((current->flags & PF_NPROC_EXCEEDED) &&atomic_read(&cred->user->processes) > rlimit(RLIMIT_NPROC)) {retval = -EAGAIN;goto out_ret;}/* We're below the limit (still or again), so we don't want to make* further execve() calls fail. */current->flags &= ~PF_NPROC_EXCEEDED;## 2. 调用unshare_files方法,这部分大概是说取消一些文件的进程共享,但具体的细节不太了解了。文章中说它是为了防止可能的内存溢出retval = unshare_files(&displaced);if (retval)goto out_ret;retval = -ENOMEM;## 3.准备bprm,bprm的类型是linux_binprm,定义在https://github.com/torvalds/linux/blob/v3.10/include/linux/binfmts.h。它的作用是管理加载二进制文件时的相关参数,例如它有vma字段及对应的vm_area_struct类型,来管理程序加载后可以使用的连续地址空间(我不太熟悉这里,说的不一定对)
### 3.1 分配地址空间bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);if (!bprm)goto out_files;
### 3.2 准备bprm的credentialsretval = prepare_bprm_creds(bprm);if (retval)goto out_free;
### 3.2 对要执行的文件进程安全检查retval = check_unsafe_exec(bprm);if (retval < 0)goto out_free;clear_in_exec = retval;
### 3.3 设置进程为in_execve状态current->in_execve = 1;
### 3.4 打开给定的二进制文件file = open_exec(filename);retval = PTR_ERR(file);if (IS_ERR(file))goto out_unmark;
### 3.5 调用sched_exec()找到最小负载的CPU,用来执行该二进制文件,并设定bprm的文件相关参数sched_exec();bprm->file = file;bprm->filename = filename;bprm->interp = filename;
### 3.6 做内存、参数、环境变量等相关的初始化工作retval = bprm_mm_init(bprm);if (retval)goto out_file;bprm->argc = count(argv, MAX_ARG_STRINGS);if ((retval = bprm->argc) < 0)goto out;bprm->envc = count(envp, MAX_ARG_STRINGS);if ((retval = bprm->envc) < 0)goto out;retval = prepare_binprm(bprm);if (retval < 0)goto out;retval = copy_strings_kernel(1, &bprm->filename, bprm);if (retval < 0)goto out;bprm->exec = bprm->p;retval = copy_strings(bprm->envc, envp, bprm);if (retval < 0)goto out;retval = copy_strings(bprm->argc, argv, bprm);if (retval < 0)goto out;
## 4. 执行search_binary_handler方法retval = search_binary_handler(bprm);if (retval < 0)goto out;

四、各binfmt模块阶段

  • search_binary_handler方法会调用load_binary方法列出系统支持的二进制文件格式,并根据当前的linux_binprm检查是否在支持列表中。https://github.com/torvalds/linux/blob/v3.10/fs/exec.c#L1369
  • 当前Linux内核支持的二进制格式有:
    • binfmt_script - 以#!行开头的脚本文件 https://github.com/torvalds/linux/blob/v3.10/fs/binfmt_script.c
    • binfmt_misc - 根据内核运行时配置,声明的不同二进制文件格式 https://github.com/torvalds/linux/blob/v3.10/fs/binfmt_misc.c
    • binfmt_elf - ELF格式 https://github.com/torvalds/linux/blob/v3.10/fs/binfmt_elf.c
    • binfmt_aout - a.out格式 https://github.com/torvalds/linux/blob/v3.10/fs/binfmt_aout.c
    • binfmt_elf_fdpic - ELF FDPIC二进制 https://github.com/torvalds/linux/blob/v3.10/fs/binfmt_elf_fdpic.c
    • binfmt_em86 - 运行在Alpha主机中的Intel ELF二进制 https://github.com/torvalds/linux/blob/v3.10/fs/binfmt_em86.c
    • binfmt_som - HP-UX的SOM格式
    • binfmt_flat - 神奇的一个格式,我以前从来没有看到过。从Google中没有找到任何有用的信息,然后以我费劲的看了一下源码,猜它的用途是把文件里的内容直接扔到内存里,然后直接执行,完全没有任何编译、处理的过程。当然我不太会C,猜的不一定对。
  • 每一个上面的binfmt文件,会根据自己的情况判断给定的文件是否为自己支持的格式。
  • 比如我们用到最多的binfmt_elf
## 在https://github.com/torvalds/linux/blob/v3.10/include/uapi/linux/elf.h#L339
## 定义了ELFMAG常量,也就是ELF的Magic Number
#define ELFMAG0     0x7f        /* EI_MAG */
#define ELFMAG1     'E'
#define ELFMAG2     'L'
#define ELFMAG3     'F'
#define ELFMAG      "\177ELF"
#define SELFMAG     4## 在https://github.com/torvalds/linux/blob/v3.10/fs/binfmt_elf.c的load_elf_binary方法中
## 使用了此Magic Number来检查为合法的ELF文件/* First of all, some simple consistency checks */if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)goto out;
  • 比如用到的情况也非常非常多的binfmt_script,则在模块里直接写了检查的逻辑
 if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))return -ENOEXEC;
  • 然后接下来就是各个模块,根据自己的情况,来执行自己格式文件的代码了。

大概是这个样子,我简单的画了个图,可以对照着看一下。

load_elf_binary

ELF文件的加载过程(load_elf_binary函数详解)--Linux进程的管理与调度(十三)

ELF文件的加载过程(load_elf_binary函数详解)--Linux进程的管理与调度(十三)_OSKernelLAB(gatieme)-CSDN博客_elf文件加载过程

日期 内核版本 架构 作者 GitHub CSDN
2016-06-04 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度-之-进程的描述

加载和动态链接


从编译/链接和运行的角度看,应用程序和库程序的连接有两种方式。
一种是固定的、静态的连接,就是把需要用到的库函数的目标代码(二进制)代码从程序库中抽取出来,链接进应用软件的目标映像中;

另一种是动态链接,是指库函数的代码并不进入应用软件的目标映像,应用软件在编译/链接阶段并不完成跟库函数的链接,而是把函数库的映像也交给用户,到启动应用软件目标映像运行时才把程序库的映像也装入用户空间(并加以定位),再完成应用软件与库函数的连接。

这样,就有了两种不同的ELF格式映像。

  • 一种是静态链接的,在装入/启动其运行时无需装入函数库映像、也无需进行动态连接。

  • 另一种是动态连接,需要在装入/启动其运行时同时装入函数库映像并进行动态链接。

Linux内核既支持静态链接的ELF映像,也支持动态链接的ELF映像,而且装入/启动ELF映像必需由内核完成,而动态连接的实现则既可以在内核中完成,也可在用户空间完成。

因此,GNU把对于动态链接ELF映像的支持作了分工:

把ELF映像的装入/启动入在Linux内核中;而把动态链接的实现放在用户空间(glibc),并为此提供一个称为”解释器”(ld-linux.so.2)的工具软件,而解释器的装入/启动也由内核负责,这在后面我们分析ELF文件的加载时就可以看到

这部分主要说明ELF文件在内核空间的加载过程,下一部分对用户空间符号的动态解析过程进行说明。

Linux可执行文件类型的注册机制


在说明ELF文件的加载过程以前,我们先回答一个问题,就是:

为什么Linux可以运行ELF文件?

内核对所支持的每种可执行的程序类型都有个struct linux_binfmt的数据结构,这个结构我们在前面的博文中我们已经提到, 但是没有详细讲. 其定义如下

/** This structure defines the functions that are used to load the binary formats that* linux accepts.*/
struct linux_binfmt {struct list_head lh;struct module *module;int (*load_binary)(struct linux_binprm *);int (*load_shlib)(struct file *);int (*core_dump)(struct coredump_params *cprm);unsigned long min_coredump;     /* minimal dump size */};

linux_binfmt定义在include/linux/binfmts.h中

linux支持其他不同格式的可执行程序, 在这种方式下, linux能运行其他操作系统所编译的程序, 如MS-DOS程序, 或BSD Unix的COFF可执行格式, 因此linux内核用struct linux_binfmt来描述各种可执行程序。

linux内核对所支持的每种可执行的程序类型都有个struct linux_binfmt的数据结构,
其提供了3种方法来加载和执行可执行程序

函数 描述
load_binary 通过读存放在可执行文件中的信息为当前进程建立一个新的执行环境
load_shlib 用于动态的把一个共享库捆绑到一个已经在运行的进程, 这是由uselib()系统调用激活的
core_dump 在名为core的文件中, 存放当前进程的执行上下文. 这个文件通常是在进程接收到一个缺省操作为”dump”的信号时被创建的, 其格式取决于被执行程序的可执行类型

所有的linux_binfmt对象都处于一个链表中, 第一个元素的地址存放在formats变量中, 可以通过调用register_binfmt()和unregister_binfmt()函数在链表中插入和删除元素, 在系统启动期间, 为每个编译进内核的可执行格式都执行registre_fmt()函数. 当实现了一个新的可执行格式的模块正被装载时, 也执行这个函数, 当模块被卸载时, 执行unregister_binfmt()函数.

当我们执行一个可执行程序的时候, 内核会list_for_each_entry遍历所有注册的linux_binfmt对象, 对其调用load_binrary方法来尝试加载, 直到加载成功为止.

其中的load_binary函数指针指向的就是一个可执行程序的处理函数。而我们研究的ELF文件格式的linux_binfmt结构对象elf_format, 定义如下, 在/fs/binfmt.c中

static struct linux_binfmt elf_format = {.module      = THIS_MODULE,.load_binary = load_elf_binary,.load_shlib      = load_elf_library,.core_dump       = elf_core_dump,.min_coredump    = ELF_EXEC_PAGESIZE,.hasvdso     = 1
};

要支持ELF文件的运行,则必须向内核登记注册elf_format这个linux_binfmt类型的数据结构,加入到内核支持的可执行程序的队列中。内核提供两个函数来完成这个功能,一个注册,一个注销,即:

int register_binfmt(struct linux_binfmt * fmt)
int unregister_binfmt(struct linux_binfmt * fmt)

当需要运行一个程序时,则扫描这个队列,依次调用各个数据结构所提供的load处理程序来进行加载工作,ELF中加载程序即为load_elf_binary,内核中已经注册的可运行文件结构linux_binfmt会让其所属的加载程序load_binary逐一前来认领需要运行的程序binary,如果某个格式的处理程序发现相符后,便执行该格式映像的装入和启动

内核空间的加载过程load_elf_binary


内核中实际执行execv()或execve()系统调用的程序是do_execve(),这个函数先打开目标映像文件,并从目标文件的头部(第一个字节开始)读入若干(当前Linux内核中是128)字节(实际上就是填充ELF文件头,下面的分析可以看到),然后调用另一个函数search_binary_handler(),在此函数里面,它会搜索我们上面提到的Linux支持的可执行文件类型队列,让各种可执行程序的处理程序前来认领和处理。如果类型匹配,则调用load_binary函数指针所指向的处理函数来处理目标映像文件。

在ELF文件格式中,处理函数是load_elf_binary函数,下面主要就是分析load_elf_binary函数的执行过程(说明:因为内核中实际的加载需要涉及到很多东西,这里只关注跟ELF文件的处理相关的代码)

其流程如下

  1. 填充并且检查目标程序ELF头部

  2. load_elf_phdrs加载目标程序的程序头表

  3. 如果需要动态链接, 则寻找和处理解释器段

  4. 检查并读取解释器的程序表头

  5. 装入目标程序的段segment

  6. 填写程序的入口地址

  7. create_elf_tables填写目标文件的参数环境变量等必要信息

  8. start_kernel宏准备进入新的程序入口

填充并且检查目标程序ELF头部


struct pt_regs *regs = current_pt_regs();

struct pt_regs *regs = current_pt_regs();
struct {struct elfhdr elf_ex;struct elfhdr interp_elf_ex;
} *loc;
struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;loc = kmalloc(sizeof(*loc), GFP_KERNEL);
if (!loc) {retval = -ENOMEM;goto out_ret;
}/* Get the exec-header使用映像文件的前128个字节对bprm->buf进行了填充  */
loc->elf_ex = *((struct elfhdr *)bprm->buf);retval = -ENOEXEC;
/* First of all, some simple consistency checks比较文件头的前四个字节。*/
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)goto out;
/*  还要看映像的类型是否ET_EXEC和ET_DYN之一;前者表示可执行映像,后者表示共享库  */
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)goto out;

在load_elf_binary之前,内核已经使用映像文件的前128个字节对bprm->buf进行了填充,563行就是使用这此信息填充映像的文件头(具体数据结构定义见第一部分,ELF文件头节),然后567行就是比较文件头的前四个字节,查看是否是ELF文件类型定义的“\177ELF”。除这4个字符以外,还要看映像的类型是否ET_EXEC和ET_DYN之一;前者表示可执行映像,后者表示共享库。

load_elf_phdrs加载目标程序的程序头表


    elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);if (!elf_phdata)goto out;

而这个load_elf_phdrs函数就是通过kernel_read读入整个program header table。从函数代码中可以看到,一个可执行程序必须至少有一个段(segment),而所有段的大小之和不能超过64K(65536u)

/*** load_elf_phdrs() - load ELF program headers* @elf_ex:   ELF header of the binary whose program headers should be loaded* @elf_file: the opened ELF binary file** Loads ELF program headers from the binary file elf_file, which has the ELF* header pointed to by elf_ex, into a newly allocated array. The caller is* responsible for freeing the allocated data. Returns an ERR_PTR upon failure.*/
static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex,struct file *elf_file)
{struct elf_phdr *elf_phdata = NULL;int retval, size, err = -1;/** If the size of this structure has changed, then punt, since* we will be doing the wrong thing.*/if (elf_ex->e_phentsize != sizeof(struct elf_phdr))goto out;/* Sanity check the number of program headers... */if (elf_ex->e_phnum < 1 ||elf_ex->e_phnum > 65536U / sizeof(struct elf_phdr))goto out;/* ...and their total size. */size = sizeof(struct elf_phdr) * elf_ex->e_phnum;if (size > ELF_MIN_ALIGN)goto out;elf_phdata = kmalloc(size, GFP_KERNEL);if (!elf_phdata)goto out;/* Read in the program headers */retval = kernel_read(elf_file, elf_ex->e_phoff,(char *)elf_phdata, size);if (retval != size) {err = (retval < 0) ? retval : -EIO;goto out;}/* Success! */err = 0;
out:if (err) {kfree(elf_phdata);elf_phdata = NULL;}return elf_phdata;
}

如果需要动态链接, 则寻找和处理解释器段


这个for循环的目的在于寻找和处理目标映像的”解释器”段。

“解释器”段的类型为PT_INTERP,

找到后就根据其位置的p_offset和大小p_filesz把整个”解释器”段的内容读入缓冲区。

“解释器”段实际上只是一个字符串,

即解释器的文件名,如”/lib/ld-linux.so.2”, 或者64位机器上对应的叫做”/lib64/ld-linux-x86-64.so.2”

有了解释器的文件名以后,就通过open_exec()打开这个文件,再通过kernel_read()读入其开关128个字节,即解释器映像的头部。*

    for (i = 0; i < loc->elf_ex.e_phnum; i++) {/*  3.1  检查是否有需要加载的解释器  */if (elf_ppnt->p_type == PT_INTERP) {/* This is the program interpreter used for* shared libraries - for now assume that this* is an a.out format binary*//*  3.2 根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区  */retval = kernel_read(bprm->file, elf_ppnt->p_offset,elf_interpreter,elf_ppnt->p_filesz);if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')goto out_free_interp;/*  3.3 通过open_exec()打开解释器文件 */interpreter = open_exec(elf_interpreter);/* Get the exec headers 3.4  通过kernel_read()读入解释器的前128个字节,即解释器映像的头部。*/retval = kernel_read(interpreter, 0,(void *)&loc->interp_elf_ex,sizeof(loc->interp_elf_ex));break;}elf_ppnt++;}
  • 可以使用readelf -l查看program headers, 其中的INTERP段标识了我们程序所需要的解释器
readelf -l testelf_normal
  • 1

readelf -l testelf_dynamic
  • 1

readelf -l test_static
  • 1

我们可以看到testelf_normal和testelf_dynamic都是动态链接的需要解释器

而testelf_static则是静态链接的不需要解释器

检查并读取解释器的程序表头


如果需要加载解释器, 前面经过一趟for循环已经找到了需要的解释器信息elf_interpreter, 他也是当作一个ELF文件, 因此跟目标可执行程序一样, 我们需要load_elf_phdrs加载解释器的程序头表program header table

    /*   4.    检查并读取解释器的程序表头 *//* Some simple consistency checks for the interpreter 4.1  检查解释器头的信息  */if (elf_interpreter) {retval = -ELIBBAD;/* Not an ELF interpreter *//* Load the interpreter program headers4.2  读入解释器的程序头*/interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,interpreter);if (!interp_elf_phdata)goto out_free_dentry;

至此我们已经把目标执行程序和其所需要的解释器都加载初始化, 并且完成检查工作, 也加载了程序头表program header table, 下面开始加载程序的段信息

装入目标程序的段segment


这段代码从目标映像的程序头中搜索类型为PT_LOAD的段(Segment)。在二进制映像中,只有类型为PT_LOAD的段才是需要装入的。当然在装入之前,需要确定装入的地址,只要考虑的就是页面对齐,还有该段的p_vaddr域的值(上面省略这部分内容)。确定了装入地址后,就通过elf_map()建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射,其返回值就是实际映射的起始地址。

    */for(i = 0, elf_ppnt = elf_phdata;i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {/*  5.1   搜索PT_LOAD的段, 这个是需要装入的 */if (elf_ppnt->p_type != PT_LOAD)continue;/* 5.2  检查地址和页面的信息  */// ......////*  5.3  虚拟地址空间与目标映像文件的映射确定了装入地址后,就通过elf_map()建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射,其返回值就是实际映射的起始地址 */error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,elf_prot, elf_flags, total_size);}

填写程序的入口地址


完成了目标程序和解释器的加载, 同时目标程序的各个段也已经加载到内存了, 我们的目标程序已经准备好了要执行了, 但是还缺少一样东西, 就是我们程序的入口地址, 没有入口地址, 操作系统就不知道从哪里开始执行内存中加载好的可执行映像

这段程序的逻辑非常简单:
如果需要装入解释器,就通过load_elf_interp装入其映像, 并把将来进入用户空间的入口地址设置成load_elf_interp()的返回值,即解释器映像的入口地址。
而若不装入解释器,那么这个入口地址就是目标映像本身的入口地址。

if (elf_interpreter) {unsigned long interp_map_addr = 0;elf_entry = load_elf_interp(&loc->interp_elf_ex,interpreter,&interp_map_addr,load_bias, interp_elf_phdata);/*  入口地址是解释器映像的入口地址  */} else {/*  入口地址是目标程序的入口地址  */elf_entry = loc->elf_ex.e_entry;}}

create_elf_tables填写目标文件的参数环境变量等必要信息


在完成装入,启动用户空间的映像运行之前,还需要为目标映像和解释器准备好一些有关的信息,这些信息包括常规的argc、envc等等,还有一些“辅助向量(Auxiliary Vector)”。这些信息需要复制到用户空间,使它们在CPU进入解释器或目标映像的程序入口时出现在用户空间堆栈上。这里的create_elf_tables()就起着这个作用。(HY:将参数和环境变量put到用户空间的堆栈上)

    install_exec_creds(bprm);retval = create_elf_tables(bprm, &loc->elf_ex,load_addr, interp_load_addr);if (retval < 0)goto out;/* N.B. passed_fileno might not be initialized? */current->mm->end_code = end_code;current->mm->start_code = start_code;current->mm->start_data = start_data;current->mm->end_data = end_data;current->mm->start_stack = bprm->p;

HY:

/* N.B. passed_fileno might not be initialized? */
    current->mm->end_code = end_code;
    current->mm->start_code = start_code;
    current->mm->start_data = start_data;
    current->mm->end_data = end_data;
    current->mm->start_stack = bprm->p;

填充task_struct->mm结构体。

start_thread宏准备进入新的程序入口


最后,start_thread()这个宏操作会将eip和esp(HY:EIP指令指針pc,ESP堆栈指针sp)改成新的地址,就使得CPU在返回用户空间时就进入新的程序入口。如果存在解释器映像,那么这就是解释器映像的程序入口,否则就是目标映像的程序入口。那么什么情况下有解释器映像存在,什么情况下没有呢?如果目标映像与各种库的链接是静态链接,因而无需依靠共享库、即动态链接库,那就不需要解释器映像;否则就一定要有解释器映像存在。
start_thread宏是一个体系结构相关的函数,请定义可以参照http://lxr.free-electrons.com/ident?v=4.6;i=start_thread

附录(load_elf_banray函数注释)



  • 1

总结


简单来说可以分成这几步

  1. 读取并检查目标可执行程序的头信息, 检查完成后加载目标程序的程序头表

  2. 如果需要解释器则读取并检查解释器的头信息, 检查完成后加载解释器的程序头表

  3. 装入目标程序的段segment, 这些才是目标程序二进制代码中的真正可执行映像

  4. 填写程序的入口地址(如果有解释器则填入解释器的入口地址, 否则直接填入可执行程序的入口地址)

  5. create_elf_tables填写目标文件的参数环境变量等必要信息

  6. start_thread宏准备进入新的程序入口

gcc在编译时,除非显示的使用static标签,否则所有程序的链接都是动态链接的,也就是说需要解释器。由此可见,我们的程序在被内核加载到内存,内核跳到用户空间后并不是执行目标程序的,而是先把控制权交到用户空间的解释器,由解释器加载运行用户程序所需要的动态库(比如libc等等),然后控制权才会转移到用户程序。

ELF文件中符号的动态解析过程


前面我们提到了内核空间中ELF文件的加载工作

内核的工作

  1. 内核首先读取ELF文件头部,再读如各种数据结构,从这些数据结构中可知各段或节的地址及标识,然后调用mmap()把找到的可加载段的内容加载到内存中。同时读取段标记,以标识该段在内存中是否可读、可写、可执行。其中,文本段是程序代码,只读且可执行,而数据段是可读且可写。

  2. 从PT_INTERP的段中找到所对应的动态链接器名称,并加载动态链接器。通常是/lib/ld-linux.so.2.

  3. 内核把新进程的堆栈中设置一些标记对,以指示动态链接器的相关操作。

  4. 内核把控制权传递给动态链接器。

动态链接器的工作并不是在内核空间完成的, 而是在用户空间完成的, 比如C语言程序则交给C运行时库来完成, 这个并不是我们今天内核学习的重点, 而是由glic完成的,但是其一般过程如下

动态链接器的工作

  1. 动态链接器检查程序对共享库的依赖性,并在需要时对其进行加载。

  2. 动态链接器对程序的外部引用进行重定位,并告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态链接还有一个延迟定位的特性,即只有在“真正”需要引用符号时才重定位,这对提高程序运行效率有极大帮助。

  3. 动态链接器执行在ELF文件中标记为.init的节的代码,进行程序运行的初始化。
    动态链接器把控制传递给程序,从ELF文件头部中定义的程序进入点(main)开始执行。在a.out格式和ELF格式中,程序进入点的值是显式存在的,而在COFF格式中则是由规范隐含定义。

  4. 程序开始执行

具体的信息可以参照

Intel平台下Linux中ELF文件动态链接的加载、解析及实例分析(一): 加载

Intel平台下linux中ELF文件动态链接的加载、解析及实例分析(二): 函数解析与卸载

附录(load_elf_binary函数注释)


static int load_elf_binary(struct linux_binprm *bprm)
{   struct file *interpreter = NULL; /* to shut gcc up */unsigned long load_addr = 0, load_bias = 0;int load_addr_set = 0;char * elf_interpreter = NULL;unsigned long error;struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;unsigned long elf_bss, elf_brk;int retval, i;unsigned long elf_entry;unsigned long interp_load_addr = 0;unsigned long start_code, end_code, start_data, end_data;unsigned long reloc_func_desc __maybe_unused = 0;int executable_stack = EXSTACK_DEFAULT;/*  从寄存器重获取参数信息  */struct pt_regs *regs = current_pt_regs();struct {struct elfhdr elf_ex;struct elfhdr interp_elf_ex;} *loc;struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;loc = kmalloc(sizeof(*loc), GFP_KERNEL);if (!loc) {retval = -ENOMEM;goto out_ret;}/*  1  填充并且检查ELF头部  *//* Get the exec-header1.1   填充ELF头信息在load_elf_binary之前内核已经使用映像文件的前128个字节对bprm->buf进行了填充, 这里使用这此信息填充映像的文件头*/loc->elf_ex = *((struct elfhdr *)bprm->buf);retval = -ENOEXEC;/* 1.2 First of all, some simple consistency checks 比较文件头的前四个字节,查看是否是ELF文件类型定义的"\177ELF"*/if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)goto out;/*  1.3 除前4个字符以外,还要看映像的类型是否ET_EXEC和ET_DYN之一;前者表示可执行映像,后者表示共享库*/if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)goto out;/*  1.4 检查特定的目标机器标识  */if (!elf_check_arch(&loc->elf_ex))goto out;if (!bprm->file->f_op->mmap)goto out;/* 2.   load_elf_phdrs 加载程序头表load_elf_phdrs函数就是通过kernel_read读入整个program header table从函数代码中可以看到,一个可执行程序必须至少有一个段(segment),而所有段的大小之和不能超过64K。*/elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);if (!elf_phdata)goto out;/*  bss段,brk段先初始化为0  */elf_ppnt = elf_phdata;elf_bss = 0;elf_brk = 0;/*  code代码段 */start_code = ~0UL;end_code = 0;/*  data数据段 */start_data = 0;end_data = 0;/*3.   寻找和处理解释器段这个for循环的目的在于寻找和处理目标映像的"解释器"段。"解释器"段的类型为PT_INTERP,找到后就根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区。"解释器"段实际上只是一个字符串,即解释器的文件名,如"/lib/ld-linux.so.2"。有了解释器的文件名以后,就通过open_exec()打开这个文件,再通过kernel_read()读入其开关128个字节,即解释器映像的头部。*/for (i = 0; i < loc->elf_ex.e_phnum;/*  e_phnumc存储了程序头表的数目*/i++) { /*  3.1 解释器"段的类型为PT_INTERP  */if (elf_ppnt->p_type == PT_INTERP) {/* This is the program interpreter used for* shared libraries - for now assume that this* is an a.out format binary*/retval = -ENOEXEC;if (elf_ppnt->p_filesz > PATH_MAX || elf_ppnt->p_filesz < 2)goto out_free_ph;retval = -ENOMEM;/* 为动态连接器分配空间并读取加载 */elf_interpreter = kmalloc(elf_ppnt->p_filesz,GFP_KERNEL);if (!elf_interpreter)goto out_free_ph;/*  3.2 根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区  */retval = kernel_read(bprm->file, elf_ppnt->p_offset,elf_interpreter,elf_ppnt->p_filesz);if (retval != elf_ppnt->p_filesz) {if (retval >= 0)retval = -EIO;goto out_free_interp;}/* make sure path is NULL terminated */retval = -ENOEXEC;if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')goto out_free_interp;/*  3.3 通过open_exec()打开解释器文件内核把新进程的堆栈中设置一些标记对,以指示动态链接器的相关操作,详见open_exec实现 */interpreter = open_exec(elf_interpreter);retval = PTR_ERR(interpreter);if (IS_ERR(interpreter))goto out_free_interp;/** If the binary is not readable then enforce* mm->dumpable = 0 regardless of the interpreter's* permissions.*/would_dump(bprm, interpreter);/* Get the exec headers 3.4  通过kernel_read()读入解释器的前128个字节,即解释器映像的头部。*/retval = kernel_read(interpreter, 0,(void *)&loc->interp_elf_ex,sizeof(loc->interp_elf_ex));if (retval != sizeof(loc->interp_elf_ex)) {if (retval >= 0)retval = -EIO;goto out_free_dentry;}break;}/* 循环检查所有的程序头看是否有动态连接器 */elf_ppnt++;}elf_ppnt = elf_phdata;for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)switch (elf_ppnt->p_type) {case PT_GNU_STACK:if (elf_ppnt->p_flags & PF_X)executable_stack = EXSTACK_ENABLE_X;elseexecutable_stack = EXSTACK_DISABLE_X;break;case PT_LOPROC ... PT_HIPROC:retval = arch_elf_pt_proc(&loc->elf_ex, elf_ppnt,bprm->file, false,&arch_state);if (retval)goto out_free_dentry;break;}/*   4.    检查并读取解释器的程序表头 *//* Some simple consistency checks for the interpreter 4.1  检查解释器头的信息  *//* 检查是否由动态连接器,无论是否有动态连接器都会执行elf文件 */if (elf_interpreter) {retval = -ELIBBAD;/* Not an ELF interpreter */if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)goto out_free_dentry;/* Verify the interpreter has a valid arch */if (!elf_check_arch(&loc->interp_elf_ex))goto out_free_dentry;/* Load the interpreter program headers4.2  读入解释器的程序头*/interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,interpreter);if (!interp_elf_phdata)goto out_free_dentry;/* Pass PT_LOPROC..PT_HIPROC headers to arch code */elf_ppnt = interp_elf_phdata;for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++)switch (elf_ppnt->p_type) {case PT_LOPROC ... PT_HIPROC:retval = arch_elf_pt_proc(&loc->interp_elf_ex,elf_ppnt, interpreter,true, &arch_state);if (retval)goto out_free_dentry;break;}}/** Allow arch code to reject the ELF at this point, whilst it's* still possible to return an error to the code that invoked* the exec syscall.*/retval = arch_check_elf(&loc->elf_ex,!!interpreter, &loc->interp_elf_ex,&arch_state);if (retval)goto out_free_dentry;/*  Flush all traces of the currently running executable在此清除掉了父进程的所有相关代码 */retval = flush_old_exec(bprm);if (retval)goto out_free_dentry;/* Do this immediately, since STACK_TOP as used in setup_arg_pagesmay depend on the personality.  *//* 设置elf可执行文件的特性 */SET_PERSONALITY2(loc->elf_ex, &arch_state);if (elf_read_implies_exec(loc->elf_ex, executable_stack))current->personality |= READ_IMPLIES_EXEC;if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)current->flags |= PF_RANDOMIZE;setup_new_exec(bprm);/* Do this so that we can load the interpreter, if need be.  We willchange some of these later为下面的动态连接器执行获取内核空间page */retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),executable_stack);if (retval < 0)goto out_free_dentry;current->mm->start_stack = bprm->p;/* Now we do a little grungy work by mmapping the ELF image intothe correct location in memory.5  装入目标程序的段segment 这段代码从目标映像的程序头中搜索类型为PT_LOAD的段(Segment)。在二进制映像中,只有类型为PT_LOAD的段才是需要装入的。当然在装入之前,需要确定装入的地址,只要考虑的就是页面对齐,还有该段的p_vaddr域的值(上面省略这部分内容)。确定了装入地址后,就通过elf_map()建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射,其返回值就是实际映射的起始地址。*//* 按照先前获取的程序头表,循环将所有的可执行文件加载到内存中 */for(i = 0, elf_ppnt = elf_phdata;i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {int elf_prot = 0, elf_flags;unsigned long k, vaddr;unsigned long total_size = 0;/*  5.1   搜索PT_LOAD的段, 这个是需要装入的 */if (elf_ppnt->p_type != PT_LOAD)continue;if (unlikely (elf_brk > elf_bss)) {unsigned long nbyte;/* 5.2  检查地址和页面的信息  *//* There was a PT_LOAD segment with p_memsz > p_fileszbefore this one. Map anonymous pages, if needed,and clear the area.  */retval = set_brk(elf_bss + load_bias,elf_brk + load_bias);if (retval)goto out_free_dentry;nbyte = ELF_PAGEOFFSET(elf_bss);if (nbyte) {nbyte = ELF_MIN_ALIGN - nbyte;if (nbyte > elf_brk - elf_bss)nbyte = elf_brk - elf_bss;if (clear_user((void __user *)elf_bss +load_bias, nbyte)) {/** This bss-zeroing can fail if the ELF* file specifies odd protections. So* we don't check the return value*/}}}if (elf_ppnt->p_flags & PF_R)elf_prot |= PROT_READ;if (elf_ppnt->p_flags & PF_W)elf_prot |= PROT_WRITE;if (elf_ppnt->p_flags & PF_X)elf_prot |= PROT_EXEC;elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;vaddr = elf_ppnt->p_vaddr;if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {elf_flags |= MAP_FIXED;} else if (loc->elf_ex.e_type == ET_DYN) {/* Try and get dynamic programs out of the way of the* default mmap base, as well as whatever program they* might try to exec.  This is because the brk will* follow the loader, and is not movable.  */load_bias = ELF_ET_DYN_BASE - vaddr;if (current->flags & PF_RANDOMIZE)load_bias += arch_mmap_rnd();load_bias = ELF_PAGESTART(load_bias);total_size = total_mapping_size(elf_phdata,loc->elf_ex.e_phnum);if (!total_size) {retval = -EINVAL;goto out_free_dentry;}}/*  5.3  虚拟地址空间与目标映像文件的映射确定了装入地址后,就通过elf_map()建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射,其返回值就是实际映射的起始地址 */error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,elf_prot, elf_flags, total_size);if (BAD_ADDR(error)) {retval = IS_ERR((void *)error) ?PTR_ERR((void*)error) : -EINVAL;goto out_free_dentry;}if (!load_addr_set) {load_addr_set = 1;load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset);if (loc->elf_ex.e_type == ET_DYN) {load_bias += error -ELF_PAGESTART(load_bias + vaddr);load_addr += load_bias;reloc_func_desc = load_bias;}}k = elf_ppnt->p_vaddr;if (k < start_code)start_code = k;if (start_data < k)start_data = k;/** Check to see if the section's size will overflow the* allowed task size. Note that p_filesz must always be* <= p_memsz so it is only necessary to check p_memsz.*/if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz ||elf_ppnt->p_memsz > TASK_SIZE ||TASK_SIZE - elf_ppnt->p_memsz < k) {/* set_brk can never work. Avoid overflows. */retval = -EINVAL;goto out_free_dentry;}k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;if (k > elf_bss)elf_bss = k;if ((elf_ppnt->p_flags & PF_X) && end_code < k)end_code = k;if (end_data < k)end_data = k;k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;if (k > elf_brk)elf_brk = k;}/* 更新读入内存中相关信息的记录 */loc->elf_ex.e_entry += load_bias;elf_bss += load_bias;elf_brk += load_bias;start_code += load_bias;end_code += load_bias;start_data += load_bias;end_data += load_bias;/* Calling set_brk effectively mmaps the pages that we need* for the bss and break sections.  We must do this before* mapping in the interpreter, to make sure it doesn't wind* up getting placed where the bss needs to go.*//* 使用set_brk调整bss段的大小 */retval = set_brk(elf_bss, elf_brk);if (retval)goto out_free_dentry;if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) {retval = -EFAULT; /* Nobody gets to see this, but.. */goto out_free_dentry;}/*6  填写程序的入口地址这段程序的逻辑非常简单:如果需要装入解释器,就通过load_elf_interp装入其映像, 并把将来进入用户空间的入口地址设置成load_elf_interp()的返回值,即解释器映像的入口地址。而若不装入解释器,那么这个入口地址就是目标映像本身的入口地址。*/if (elf_interpreter) {/*  存在动态链接器内核把控制权传递给动态链接器。动态链接器检查程序对共享库的依赖性,并在需要时对其进行加载,由load_elf_interp完成 unsigned long interp_map_addr = 0;elf_entry = load_elf_interp(&loc->interp_elf_ex,interpreter,&interp_map_addr,load_bias, interp_elf_phdata);if (!IS_ERR((void *)elf_entry)) {/** load_elf_interp() returns relocation* adjustment*/interp_load_addr = elf_entry;elf_entry += loc->interp_elf_ex.e_entry;}if (BAD_ADDR(elf_entry)) {retval = IS_ERR((void *)elf_entry) ?(int)elf_entry : -EINVAL;goto out_free_dentry;}reloc_func_desc = interp_load_addr;allow_write_access(interpreter);fput(interpreter);kfree(elf_interpreter);} else {elf_entry = loc->elf_ex.e_entry;if (BAD_ADDR(elf_entry)) {retval = -EINVAL;goto out_free_dentry;}}kfree(interp_elf_phdata);kfree(elf_phdata);set_binfmt(&elf_format);#ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGESretval = arch_setup_additional_pages(bprm, !!elf_interpreter);if (retval < 0)goto out;
#endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES *//*   7  create_elf_tables填写目标文件的参数环境变量等必要信息在完成装入,启动用户空间的映像运行之前,还需要为目标映像和解释器准备好一些有关的信息,这些信息包括常规的argc、envc等等,还有一些"辅助向量(Auxiliary Vector)"。这些信息需要复制到用户空间,使它们在CPU进入解释器或目标映像的程序入口时出现在用户空间堆栈上。这里的create_elf_tables()就起着这个作用。*/install_exec_creds(bprm);/* 在内存中生成elf映射表 */retval = create_elf_tables(bprm, &loc->elf_ex,load_addr, interp_load_addr);if (retval < 0)goto out;/*  N.B. passed_fileno might not be initialized? 调整内存映射内容 */current->mm->end_code = end_code;current->mm->start_code = start_code;current->mm->start_data = start_data;current->mm->end_data = end_data;current->mm->start_stack = bprm->p;if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {current->mm->brk = current->mm->start_brk =arch_randomize_brk(current->mm);
#ifdef compat_brk_randomizedcurrent->brk_randomized = 1;
#endif}if (current->personality & MMAP_PAGE_ZERO) {/* Why this, you ask???  Well SVr4 maps page 0 as read-only,and some applications "depend" upon this behavior.Since we do not have the power to recompile these, weemulate the SVr4 behavior. Sigh. */error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,MAP_FIXED | MAP_PRIVATE, 0);}#ifdef ELF_PLAT_INIT/** The ABI may specify that certain registers be set up in special* ways (on i386 %edx is the address of a DT_FINI function, for* example.  In addition, it may also specify (eg, PowerPC64 ELF)* that the e_entry field is the address of the function descriptor* for the startup routine, rather than the address of the startup* routine itself.  This macro performs whatever initialization to* the regs structure is required as well as any relocations to the* function descriptor entries when executing dynamically links apps.*/ELF_PLAT_INIT(regs, reloc_func_desc);
#endif/*8  最后,start_thread()这个宏操作会将eip和esp改成新的地址,就使得CPU在返回用户空间时就进入新的程序入口。如果存在解释器映像,那么这就是解释器映像的程序入口,否则就是目标映像的程序入口。那么什么情况下有解释器映像存在,什么情况下没有呢?如果目标映像与各种库的链接是静态链接,因而无需依靠共享库、即动态链接库,那就不需要解释器映像;否则就一定要有解释器映像存在。对于一个目标程序, gcc在编译时,除非显示的使用static标签,否则所有程序的链接都是动态链接的,也就是说需要解释器。由此可见,我们的程序在被内核加载到内存,内核跳到用户空间后并不是执行我们程序的,而是先把控制权交到用户空间的解释器,由解释器加载运行用户程序所需要的动态库(比如libc等等),然后控制权才会转移到用户程序。*//* 开始执行程序,这时已经是子进程了 */start_thread(regs, elf_entry, bprm->p);retval = 0;
out:kfree(loc);
out_ret:return retval;/* error cleanup */
out_free_dentry:kfree(interp_elf_phdata);allow_write_access(interpreter);if (interpreter)fput(interpreter);
out_free_interp:kfree(elf_interpreter);
out_free_ph:kfree(elf_phdata);goto out;
}

在shell或bash执行一个bin文件或者脚本的流程相关推荐

  1. 如何将大量图片文件合并成一个*.bin文件

    文章目录 1.bin是啥 2. 首先将大量图片全都导成按你所要顺序编号的.bin文件 3.将这些.bin文件合成为一个.bin文件 4.问题它出现了(如果你没有出现问题此后内容可不看) 5.新的尝试 ...

  2. 如何把一个bin文件捆绑到一个可执行文件exe中?

    如何把一个bin文件捆绑到一个可执行文件exe中? Delphi / Windows SDK/API http://www.delphi2007.net/DelphiAPI/html/delphi_2 ...

  3. 嵌入式端音频开发(实战篇)之 4.2 把很多音频合成一个bin文件并统计每个音频文件的大小的方法(内含转换脚本)

    查看本系列全部文章请点击:嵌入式端音频开发系列汇总(持续更新) 查看本文全部内容:嵌入式端音频开发(实战篇)之 4.2 把很多音频合成一个bin文件并统计每个音频文件的大小的方法(内含转换脚本) 文件 ...

  4. 如何将多个bin文件合成一个bin文件?(一)

    一.使用到的软件 WinHex 二.所用文件 Bootload.bin Application.bin 三.生成目标文件 Target.bin 四.步骤 1)新建目标文件target.bin,此时文件 ...

  5. 如何用sqlplus执行一个sql文件和批量执行sql文件

    1.sqlplus执行单个SQL文件 举个例子,假设你的sql文件是test.sql ,保存路径是D:\script,那么在sqlplus里执行它,用如下语法: sqlplus> @D:\scr ...

  6. 如何将多个bin文件合成一个bin文件?(二)

    一.所需软件 J-flash ARM 二.所需合成文件 1)Bootload.hex(Bootload.bin) 2)Application.hex(Application.bin) 三.步骤 打开J ...

  7. 自动执行一个php文件,使用crontab自动执行php文件

    启动linux定时服务:service crond start 查看当前定时任务: crontab -l 添加新定时任务:crontab -e 先是测试crontab对于普通任务能否执行: */1 * ...

  8. 使用Lua执行一个Lua文件

    1.在桌面上新建一个文本文档,输入一下内容,并将文本文档重命名为hello.lua 2.打开Lua编辑器,在里面新建一个文件,另存为Test.lua,在里面输入: dofile("C:\\U ...

  9. 如何执行一个py文件_4个步骤教你学会用Pycharm如何运行.py文件,简单上手(建议收藏)...

    这篇文章主要介绍了Pycharm如何运行.py文件的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧! Pycharm时一 ...

  10. bin文件数据处理,读取一个文件数据写入另一个文件中

    C程序读取一个Bin文件数据,然后存入另一个Bin文件 最近工作遇到需要bin文件数据处理,对程序语言其实不大熟悉,还是凭着一些浅显的C语言基础,各种折磨一天时间才实现了工作需求,好了,废话不多说,直 ...

最新文章

  1. 历年双11实战经历者:我们是如何做数据库性能优化及运维-CloudDBA和天象
  2. python3 windows环境 安装加密库 crypto
  3. Design Pattern - Observer(C#)
  4. PostGresSQL简介与Windows上的安装教程
  5. MSF(六):后渗透
  6. java.lang.ClassNotFoundException: lombok.Data
  7. Requests上传文件
  8. 来电振铃时按音量键静音
  9. gitee图床失效后处理
  10. win7 便签快捷键
  11. 经过了多种方法的尝试,终于找到Quartus破解成功但是没有办法编译的解决方法
  12. 基于python的图像处理的毕业论文_图像处理毕业论文 精品
  13. EXCEL滚动条控件制作动态图表
  14. windows server 2019 安装CA-证书服务器
  15. android中android:wight详解
  16. conductor restart和rerun机制
  17. 1.认识童心派 — 电子胸牌
  18. anaconda的正确安装
  19. 商品促销倒计时效果实现
  20. 电脑连接多台Andro设备使用adb命令进行无线调试

热门文章

  1. 面试题:return和finally执行
  2. idea安装Scala插件
  3. Flash 3D 基础
  4. mac 备份android 手机通讯录导入iphone,iphone如何导出通讯录(轻松教你iPhone备份通讯录的方法)...
  5. vue 秒转换为时分秒_Vue源码全面解析二十九 parse函数(转换html模板为AST语法结构)...
  6. 总结的AngularJS1版本的一些面试问题
  7. Linux中使用Apache发布html网页
  8. Access control allow origin 简单请求和复杂请求
  9. Leetcode题目:Binary Tree Paths
  10. 搭建一个wordpress