exec系统调用 && 进程的加载过程
Posted zhushoucheng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了exec系统调用 && 进程的加载过程相关的知识,希望对你有一定的参考价值。
exec系统调用会从指定的文件中读取并加载指令,并替代当前调用进程的指令。从某种程度上来说,这样相当于丢弃了调用进程的内存,并开始执行新加载的指令。
-
exec系统调用会保留当前的文件描述符表单。所以任何在exec系统调用之前的文件描述符,例如0,1,2等。它们在新的程序中表示相同的东西。
-
通常来说exec系统调用不会返回,因为exec会完全替换当前进程的内存,相当于当前进程不复存在了,所以exec系统调用已经没有地方能返回了。
在运行shell时,我们不希望系统调用替代了Shell进程,实际上,Shell会执行fork,这是一个非常常见的Unix程序调用风格。对于那些想要运行程序,但是还希望能拿回控制权的场景,可以先执行fork系统调用,然后在子进程中调用exec。
以shell程序运行ls命令为例
int main(){
int pid;
...
if(fork() == 0){
//子进程操作
//加载新的程序后当前的内容将全部被舍弃,所以不会执行到下面打印函数
exec("ls","-al");
} else {
//父进程操作
do something...
}
printf("finish");
}
fork函数和exec函数共同组成了新进程的加载方式,这也是计算机创建新进程的一般方式(也许是唯一的方式)
下面代码展示了一个进程的内存映像究竟是如何一步一步建立的,还涉及了一些关于ELF可执行文件的知识(见附)。
希望能通过代码,让大家认识到进程实际上并没有那么神秘、复杂,对计算机的进程模型能有个更深的认识。
代码解析
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint64 argc, sz = 0, sp, ustack[MAXARG+1], stackbase;
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pagetable_t pagetable = 0, oldpagetable;
struct proc *p = myproc();
begin_op();
//获取path路径处的文件,即读取要加载的可执行文件
if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);
// Check ELF header
// 先从文件中读取elf信息
if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;
//创建一个新的页表
if((pagetable = proc_pagetable(p)) == 0)
goto bad;
// Load program into memory.
// 借助elf中的phoff属性(program section header off 程序段头结点在elf文件中的偏移量)
// 将程序所有的section写入其指定位置(在可执行程序编译时,其就指定好了哪个段在哪个逻辑地址)
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
//从文件中读取一个section header到ph中
if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
uint64 sz1;
//按照section header中的逻辑地址(ph.vaddr)和段长信息,在页表中开辟新的空间
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
goto bad;
sz = sz1;
if(ph.vaddr % PGSIZE != 0)
goto bad;
// Load a program segment into pagetable at virtual address va.
// 将segment写入到页表(即内存)中的对应位置
if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}
iunlockput(ip);
end_op();
ip = 0;
//将可执行文件的内容全部写入内存后,开始创建堆栈
p = myproc();
uint64 oldsz = p->sz;
// Allocate two pages at the next page boundary.
// Use the second as the user stack.
sz = PGROUNDUP(sz);
uint64 sz1;
//分配两个page,第二个用来充当用户栈
if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0)
goto bad;
sz = sz1;
uvmclear(pagetable, sz-2*PGSIZE);
sp = sz;
stackbase = sp - PGSIZE;
// Push argument strings, prepare rest of stack in ustack.
// 把执行参数写入到栈中
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp -= strlen(argv[argc]) + 1;
//内存对齐
sp -= sp % 16; // riscv sp must be 16-byte aligned
if(sp < stackbase)
goto bad;
//拷贝到栈中
if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[argc] = sp;
}
ustack[argc] = 0;
// push the array of argv[] pointers.
//把参数数组的指针拷入到栈中
sp -= (argc+1) * sizeof(uint64);
sp -= sp % 16;
if(sp < stackbase)
goto bad;
if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
goto bad;
// arguments to user main(argc, argv)
// argc is returned via the system call return
// value, which goes in a0.
// 把数组指针(即参数列表)写入到a1寄存器(该寄存器存储了函数第二个参数)
p->trapframe->a1 = sp;
// Save program name for debugging.
//把文件名设置成进程名
for(last=s=path; *s; s++)
if(*s == \'/\')
last = s+1;
safestrcpy(p->name, last, sizeof(p->name));
// Commit to the user image.
// 设置进程属性,并且将相应的寄存器置为初始状态
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
//A0用来存储返回值/函数参数,
return argc; // this ends up in a0, the first argument to main(argc, argv)
bad:
if(pagetable)
proc_freepagetable(pagetable, sz);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}
附:
PHP中调用外部命令的方法
在PHP中调用外部命令,可以用如下三种方法来实现:
方法一:用PHP提供的专门函数(四个):
PHP提供4个专门的执行外部命令的函数:exec(), system(), passthru(), shell_exec()
1)exec()
原型: string exec ( string $command [, array &$output [, int &$return_var ]] )
说明: exec执行系统外部命令时不会输出结果,而是返回结果的最后一行。如果想得到结果,可以使用第二个参数,让其输出到指定的数组。此数组一个记录代表输出的一行。即如果输出结果有20行,则这个数组就有20条记录,所以如果需要反复输出调用不同系统外部命令的结果,最好在输出每一条系统外部命令结果时清空这个数组unset($output),以防混乱。第三个参数用来取得命令执行的状态码,通常执行成功都是返回0。
<?php
exec("dir",$output);
print_r($output);
?>
2)system()
原型: string system ( string $command [, int &$return_var ] )
说明: system和exec的区别在于,system在执行系统外部命令时,直接将结果输出到游览器,如果执行命令成功则返回true,否则返回false。第二个参数与exec第三个参数含义一样。
<?php
system("pwd");
?>
3)passthru()
原型: void passthru ( string $command [, int &$return_var ] )
说明: passthru与system的区别,passthru直接将结果输出到游览器,不返回任何值,且其可以输出二进制,比如图像数据。第二个参数可选,是状态码。
<?php
header("Content-type:image/gif");
passthru("/usr/bin/ppm2tiff /usr/share/tk8.4/demos/images/teapot.ppm");
?>
4)shell_exec()
原型: string shell_exec ( string $cmd )
说明: 直接执行命令$cmd
<?php
$output = shell_exec('ls -lart');
echo "<pre>$output</pre>";
?>
方法二:反撇号
原型: 反撇号`(和~在同一个键)执行系统外部命令
说明: 在使用这种方法执行系统外部命令时,要确保shell_exec函数可用,否则是无法使用这种反撇号执行系统外部命令的。
<?php
echo `dir`;
?>
方法三:用popen()函数打开进程
原型: resource popen ( string $command , string $mode )
说明: 能够和命令进行交互。之前介绍的方法只能简单地执行命令,却不能与命令交互。有时须向命令输入一些东西,如在增加系统用户时,要调用su来把当前用户换到root用户,而su命令必须要在命令行上输入root的密码。这种情况下,用之前提到的方法显然是不行的。
popen( )函数打开一个进程管道来执行给定的命令,返回一个文件句柄,可以对它读和写。返回值和fopen()函数一样,返回一个文件指针。除非使用的是单一的模式打开(读or写),否则必须使用pclose()函数关闭。该指针可以被fgets(),fgetss(),fwrite()调用。出错时,返回FALSE。
<?php
error_reporting(E_ALL);
/* Add redirection so we can get stderr. */
$handle = popen('/path/to/executable 2>&1', 'r');
echo "'$handle'; " . gettype($handle) . "\n";
$read = fread($handle, 2096);
echo $read;
pclose($handle);
?>
要考虑两个问题:安全性和超时
1)安全性
由于PHP基本是用于WEB程序开发的,所以安全性成了人们考虑的一个重要方面 。于是PHP的设计者们给PHP加了一个门:安全模式。如果运行在安全模式下,那么PHP脚本中将受 到如下四个方面的限制:
执行外部命令
在打开文件时有些限制
连接MySQL数据库
基于HTTP的认证
在安全模式下,只有在特定目录中的外部程序才可以被执行,对其它程序的调用将被拒绝。这个目录可以在PhP.ini 文件中用safe_mode_exec_dir指令,或在编译PHP是加上--with-exec-dir选项来指定。
当你使用这些函数来执行系统命令时,可以使用escapeshellcmd()和escapeshellarg()函数阻止用户恶意在系统上执行命令,escapeshellcmd()针对的是执行的系统命令,而escapeshellarg()针对的是执行系统命令的参数。这两个参数有点类似addslashes()的功能。
2)超时
当执行命令的返回结果非常庞大时,可以需要考虑将返回结果输出至其他文件,再另行读取文件,这样可以显著提高程序执行的效率。
如果要执行的命令要花费很长的时间,那么应该把这个命令放到系统的后台去运行。但在默认情况下,象system()等函数要等到这个命令运行完才返回(实际上是在等命令的输出结果),这肯定会引起PHP脚本的超时。解决的办法是把命令的输出重定向到另外一个文件或流中,如:
<?php
system("/usr/local/bin/order_proc > /tmp/abc ");
?>
以上是关于exec系统调用 && 进程的加载过程的主要内容,如果未能解决你的问题,请参考以下文章
Linux进程启动过程分析do_execve(可执行程序的加载和运行)---Linux进程的管理与调度
Linux0.11内核--加载可执行二进制文件之3.exec