深入解析进程创建、终止与等待

在这里插入图片描述

前言

进程创建

fork

进程创建主要是通过fork()函数完成的,内核主要做以下几个工作:

分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度

如下图所示:
在这里插入图片描述

写时拷贝

父子进程代码是共享的,父子不进行数据的写入时,数据也是共享的,但是当任意一方试图写入时,便以写时拷贝的方式各自一份副本,如下图所示:
在这里插入图片描述
正是因为写时拷贝,所以父子进程才具有独立性。

进程终止

进程终止,会有以下三种情况:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码都没跑完,进程出现异常

退出码

之前我们写C或C++程序 时,都会在main函数的最后写上"return 0",这里0其实是告诉系统进程正确结束。我们可以用"echo $?"来查看上一个退出进程的退出码。如下所示:
在这里插入图片描述
退出码可以告诉我们最后一次执行的命令的状态,在命令结束以后,我们可以通过退出码来判断命令是执行成功的还是错误的。

错误码

除此之外,每次调用库函数也会有一个错误码,用于判断库函数是否调用失败,错误码可以通过errno查看,我们来看一个示例代码:

#include <stdio.h>    
#include <string.h>    
#include <errno.h>                                                                                                                                                               
int main()    
{    
    
  FILE *fp = fopen("./logtxt","r");    
  if(fp == NULL)    
  {    
    printf("%d: %s\n",errno,strerror(errno));    
    
  }    
  return 0;    
} 

运行以上代码,结果如下:
在这里插入图片描述
也可以将错误码进行返回。
当代码在运行期间,没有收到信号,并且退出码为0时,就代表代码正确跑完了;当代码在运行期间,没有收到信号,但是退出码非0时,就代表代码跑完了,但是结果不正确;当代码在运行期间,收到信号时,就代表代码没有跑完,进程出现了异常;
综上所述,进程执行的结果状态,可以用两个数字来表示:一个是信号,一个是退出码。这两个数字是不需要用户来维护的,当一个进程提出的时候,操作系统会将进程退出的详细信息写入到进程的task_struct结构体中,所以,进程退出,需要僵尸状态维持自己的退出状态。

退出

这里进程退出只考虑前两种情况。让进程退出,一方面main函数return的方式;另一方面,可以通过exit函数和_exit。

exit

exit函数简介如下图所示:
在这里插入图片描述
在任意地方都可以调用exit函数。

_exit

_exit函数用法与exit函数类似,只不过exit函数终止进程,会强制刷新缓冲区,但是_exit不会强制刷新缓冲区。同时,_exit是一种系统调用,exit函数底层调用的就是_exit。如下图所示:
在这里插入图片描述

进程等待

必要性

当子进程退出,父进程如果不对子进程进行处理,就会造成“僵尸进程”的问题,进而造成内存的泄漏。因此,父进程要通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。

wait

以下是一段进程等待的示例代码:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <stdlib.h>    
#include <sys/wait.h>    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    int cnt = 5;    
    while(cnt--)    
    {    
      printf("I am a child process,pid:%d\n",getpid());    
      sleep(1);    
    }    
    exit(1);    
  }    
    
  else if(id > 0)    
  {    
    pid_t rid = wait(NULL);                                                            
    if(rid == id)    
    {    
      printf("pid:%d,wait success!\n",getpid());    
    }    
  }    
    
  return 0;    
}    

运行结果如下:
在这里插入图片描述
如果父进程wait子进程,但是子进程没有退出,则父进程会阻塞在wait函数中,当子进程退出时,父进程就会回收子进程。当回收成功时,返回子进程的pid,否则返回-1。

waitpid

waitpid函数的声明如下:

pid_t waitpid(pid_t pid, int *status, int options);
返回值:

 当正常返回的时候waitpid返回收集到的⼦进程的进程ID;

 如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;

 如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:

 pid:

	 Pid=-1,等待任⼀个⼦进程。与wait等效。
	
	 Pid>0.等待其进程ID与pid相等的⼦进程。

 status: 输出型参数

 	WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)

 	WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)

 options:默认为0,表⽰阻塞等待
 WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。

当将pid设置为-1,options设置为0时,与wait函数等价。使用示例如下所示:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <stdlib.h>    
#include <sys/wait.h>    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    int cnt = 5;    
    while(cnt--)    
    {    
      printf("I am a child process,pid:%d\n",getpid());    
      sleep(1);    
    }    
    exit(1);    
  }    
    
  else if(id > 0)    
  {    
    sleep(10);    
    int status = 0;    
    
    pid_t rid = waitpid(id, &status, 0);    
    if(rid == id)    
    {    
      int exit_code = ((status >> 8) & 0xFF);    
      int exit_sig = status & 0x7F;    
    
      printf("pid:%d,wait success!status:%d,status_code:%d,exit_sig:%d\n",getpid(),status,exit_code,exit_sig);                                                                   
    }    
    sleep(5);    
  }    
    
  return 0;    
}

运行结果如下:
在这里插入图片描述
status获得的是子进程的退出信息,本质是要获得子进程提出的两个数字,stauts共有32个比特位,如下图所示:
在这里插入图片描述
其中高八位为对应的退出码,低七位为对应的信号码,可以通过右移和按位与操作获取status的前八位和后八位,得到退出码和对应的信号码。当使用"kill -9 [进程号]"杀掉子进程时,得到的信号码为9。
但是通过位操作获取对应的退出码和信号码不是很方便,因此还提供了相关宏来检测相关的信号。示例代码如下所示:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
  printf("I am a parent process,pid:%d,ppid:%d\n",getpid(),getppid());
  
  pid_t id = fork();
  if(id < 0)
  {
    exit(1);
  }

  else if(id == 0)
  {
    int cnt = 5;
    while(cnt--)
    {
      printf("I am a child process,pid:%d,ppid:%d\n",getpid(),getppid());
      sleep(1);
    }
    exit(10);
  }

  else{
    int status = 0;    
    pid_t rid = waitpid(id, &status, 0 );    
    if(rid > 0)
    {    
     if(WIFEXITED(status))    
     {                                                                                                                                                                           
    
      printf("wait success,退出的子进程是:%d,exit_code:%d\n",rid,WEXITSTATUS(status));    
     }    
     else{    
    
      printf("异常退出");
     }
    }
    else{
      printf("ret:%d\n",rid);
      perror("waitpid");
    }
  }

  return 0;
}

运行结果如下:
在这里插入图片描述

waitpid是一种系统调用,可以根据进程的PCB得到相关的信息。
当要等待的进程不是自己的子进程时,会返回-1。示例代码如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
  printf("I am a parent process,pid:%d,ppid:%d\n",getpid(),getppid());
  
  pid_t id = fork();
  if(id < 0)
  {
    exit(1);
  }

  else if(id == 0)
  {
    int cnt = 5;
    while(cnt--)
    {
      printf("I am a child process,pid:%d,ppid:%d\n",getpid(),getppid());
      sleep(1);   
    }
    exit(10);                                                            
  }               
     
  else{                                                                  
    int status = 0;
    pid_t rid = waitpid(id + 1, &status, 0 );
    if(rid > 0)                                                          
    {              
      printf("wait success,退出的子进程是:%d,exit_code:%d,exit_sig:%d\n",rid,(status >> 8) & 0xFF,status & 0x7F);
                                                                                                                                                                                 
    }              
    else{                                                                                                        
      printf("ret:%d\n",rid);
      perror("waitpid");
    }        
   }

  return 0;
}
                   

运行结果如下:
在这里插入图片描述
之前父进程等待的方式都是阻塞等待,但还有非阻塞轮询的等待方式,非阻塞等待可以使用WHOHANG宏。使用如下所示:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <stdlib.h>    
#include <sys/wait.h>    
int main()    
{    
  printf("I am a parent process,pid:%d,ppid:%d\n",getpid(),getppid());    
    
  pid_t id = fork();    
  if(id < 0)    
  {    
    exit(1);    
  }    
    
  else if(id == 0)    
  {    
    int cnt = 5;    
    while(cnt--)    
    {    
      printf("I am a child process,pid:%d,ppid:%d\n",getpid(),getppid());    
      sleep(1);    
    }    
    exit(10);    
  }    
    
  else{    
    while(1)    
    {    
      int status = 0;                                                                                                                                                                
      pid_t rid = waitpid(id, &status, WNOHANG); //非阻塞检测 && 回收        
      if(rid > 0)                                                                                                                                                                
      {                                                                          
        printf("wait success,退出的子进程是:%d,exit_code:%d\n",rid,WEXITSTATUS(status));    
      }    
      else if(rid == 0)        
      { 
        printf("子进程正在运行,父进程需要等待\n");    
        usleep(100000);
      }        
      else{    
        perror("waitpid");    
        break;
      }    
    }
  }

  return 0;
}

父进程要等待多长时间,是由子进程来决定的,阻塞等待在等待期间,什么都没干,非阻塞等待在等待期间,可以继续执行相关代码,将等待时间利用了起来。
通过waitpid函数而C++中的vector容器,我们就可以创建多个进程,示例代码如下:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <stdlib.h>    
#include <sys/wait.h>    
#include <vector>    
#include <iostream>    
enum{    
  OK,    
  USAGE_ERR    
};    
    
void Task()    
{    
  int cnt = 5;    
  while(cnt--)    
  {    
    printf("我是一个子进程,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);    
    sleep(1);    
  }    
}    
    
void CreateChildProcess(int num,std::vector<pid_t> *subs)    
{    
    
  for(int i = 0;i < num;i++)    
  {    
    pid_t id = fork();    
    if(id == 0)    
    {    
      //child    
      Task();    
      exit(0);                                                                                                                                                                   
    }    
    //父进程    
    subs->push_back(id);    
  }     
}    

void WaitAllChild(const std::vector<pid_t> &subs)
{
  for(const auto &pid : subs)                                                                                                                                                    
  {
    int status = 0;
    pid_t rid = waitpid(pid, &status, 0);
    if(rid > 0)
    {
      printf("子进程:%d Exit,exit code: %d\n",rid,WEXITSTATUS(status));
    }
  }
}

int main(int argc, char *argv[])
{
  if(argc != 2)
  {
    std::cout<< "Usage: " << argv[0] << " process_num" << std::endl;
    exit(USAGE_ERR);
  }
  
  int num = std::stoi(argv[1]);
  std::vector<pid_t> subs;
  //创建多进程
  CreateChildProcess(num,&subs);

  //父进程
  WaitAllChild(subs);

  return OK;
}

运行结果和查看相关进程如下:
在这里插入图片描述
在这里插入图片描述

进程程序替换

进程程序替换可以用来执行其它的程序,一个简单使用如下:

#include <stdio.h>
#include <unistd.h>

int main()
{
  printf("我是一个进程:%d\n",getpid());

  //执行另一个程序
  execl("/usr/bin/ls","ls","-a","-l",NULL);//程序替换函数

  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");

  return 0;
}

运行结果如下:
在这里插入图片描述
这种进程在执行过程中去执行另一个程序的代码和数据,这种过程就叫做进程替换。如下图所示:
在这里插入图片描述
当调用exec等函数时,磁盘中会出现一个新的可执行程序,会将原程序的数据段和代码段用新程序的数据和代码覆盖掉,这就是程序替换的一个基本原理。程序替换期间,只改变了物理内存的部分,进程的pid不变,因此,程序替换过程中没有创建新的进程,程序替换的本质是将代码和数据,拷贝到内存中。
之前我们也介绍过,程序运行之前,必须先加载到内存中,本质是一个IO操作,只有操作系统才能够将数据从磁盘中加载到内存,所以,操作系统必须能提供对应的系统调用,来完成加载的这个过程,程序替换的过程就是加载。
我们发现,程序替换之后,原来程序的后续代码也不执行了,这是因为代码被替换了。因此,exec系列函数,成功的时候,没有返回值。因此我们一般会创建个子进程,使子进程进行程序替换,如下所示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  printf("我是一个进程:%d\n",getpid());

  pid_t id = fork();
  if(id == 0)
  {

    //执行另一个程序
    execl("/usr/bin/ls","ls","-a","-l",NULL);//程序替换函数
    exit(0);
  }

  wait(NULL);
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");

  return 0;
}

运行结果如下:
在这里插入图片描述
默认的数据段和代码段都是只读区域,当子进程要加载数据段和代码段时,会发生写时拷贝,在其它区域加载数据段和代码段,并让子进程的页表指向对应的区域,使父子彻底分离。

execl

该函数声明如下:
在这里插入图片描述
该函数第一个参数是要执行的程序的路径,第二个参数是相关的栈帧,表明如何执行,命令行怎么写,这个参数就会怎么传,最后以NULL结尾即可。程序替换的函数,部分参数确实可以省略,但是,一般不要省略。
当替换成功时,没有返回值,也不会执行后续代码;但当出错时会返回-1,接着执行后续代码。

execlp

execlp的相关参数如下:
在这里插入图片描述
与execl不同的是,这个函数第一个参数为file,只需要告诉程序名即可,该函数会自动到环境变量PATH所表明的路径下查找。使用示例如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  printf("我是一个进程:%d\n",getpid());

  pid_t id = fork();
  if(id == 0)
  {

    //执行另一个程序
    execlp("ls","ls","-a","-l","-n",NULL);//程序替换函数
    exit(0);
  }

  wait(NULL);
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");

  return 0;
}

运行结果如下:
在这里插入图片描述

execv

execv函数声明如下:
在这里插入图片描述
execv没有可变参数,而是采用了一个指针数组,之前使用execl和execlp是将参数一个一个传入的,而这个函数可以通过传入一个指针数组参数表来传入相关的参数。使用示例如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  printf("我是一个进程:%d\n",getpid());

  pid_t id = fork();
  if(id == 0)
  { 
    //执行另一个程序
    char *const argv[]={
      (char*)"ls",
      (char*)"-a",
      (char*)"-l",
      NULL
    };
    execv("/usr/bin/ls",argv);//程序替换函数
    exit(0);
  }

  wait(NULL);
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");

  return 0;
}

运行结果如下:
在这里插入图片描述

execvp

execvp函数的声明如下:
在这里插入图片描述
execvp函数第一个参数为file,只需要告诉程序名即可,该函数会自动到环境变量PATH所表明的路径下查找。使用示例如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  printf("我是一个进程:%d\n",getpid());

  pid_t id = fork();
  if(id == 0)
  { 
    //执行另一个程序
    char *const argv[]={
      (char*)"ls",
      (char*)"-a",
      (char*)"-l",
      NULL
    };
    execvp(argv[0],argv);//程序替换函数
    exit(0);
  }

  wait(NULL);
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");

  return 0;
}

运行结果如下:
在这里插入图片描述

execvpe

execvpe函数的声明如下:
在这里插入图片描述
与之前的函数相比,这个函数多了一个环境变量相关的参数。这个参数允许在程序替换时传入全新的环境变量。使用示例如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  printf("我是一个进程:%d\n",getpid());

  pid_t id = fork();
  if(id == 0)
  { 
    //执行另一个程序
    char *const argv[]={
      (char*)"./othercmd",
      (char*)"-a",
      (char*)"-b",
      NULL
    };

    char* myenv[] = {
      "PATH=/home/zhangsan/linux/exec",
      NULL
    };
    //extern char** environ;
    execvpe("./othercmd",argv,myenv);//程序替换函数
    exit(3);
  }

  wait(NULL);
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");

  return 0;
}

othercmd是C++编译运行后生成的可执行程序,以上代码运行结果如下:
在这里插入图片描述
在这个程序中,我们就通过程序替换的方式来执行了其它可执行程序,bash进程也会通过程序替换的方式,将相关的环境变量传给子进程并执行,命令行参数和环境变量表,都是父进程通过exec系列的函数传递的。
若想给子进程传已有的环境变量,可以传入environ,该函数第三个参数可以选择传给子进程的环境变量,若传入自定义环境变量,会覆盖掉原有的环境变量。
若想在已有的环境变量上新增环境变量,使用putenv函数即可,如下所示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  printf("我是一个进程:%d\n",getpid());

  pid_t id = fork();
  if(id == 0)
  { 
    //执行另一个程序
    char *const argv[]={
      (char*)"./othercmd",
      (char*)"-a",
      (char*)"-b",
      NULL
    };

    char* myenv[] = {
      "PATH=/home/zhangsan/linux/exec",
      NULL
    };

    extern char** environ;
    putenv((char*)"haha=aaaaaaaaaaaaaaaaaaa");
    execvpe("./othercmd",argv,environ);//程序替换函数
    exit(3);
  }

  wait(NULL);
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");
  printf("代码运行中....\n");

  return 0;
}

execve

execve是exec系列函数底层调用的函数,其它函数都是库函数,execve函数是一个系统调用。

内容概要:本文详细记录了对一个Android ARM64静态ELF文件中字符串加密机制的逆向分析过程。该ELF文件的所有字符串均被加密,无法通过常规strings命令或IDA直接识别。作者通过分析发现,加密字符串存储在.rodata段,其解密所需信息(包括密文地址、长度16位密钥)保存在.data.rel.ro段的40字节描述符中。核心解密函数sub_10F408采用自反的双pass流密码算法,结合固定密钥KEY_TERM(由.data段24字节数据计算得出),实现字节级非线性、位置与长度相关的加密。文章还复现了完整的Python解密脚本,并揭示了该保护机制的本质为代码混淆而非强加密,最终成功批量解密全部956条字符串,暴露程序真实行为,如shell命令模板、设备标识篡改、网络重置等操作。此外,文中还提及未启用的自定义壳框架及其反dump设计。; 适合人群:具备逆向工程基础的安全研究人员、二进制分析人员及对ELF保护技术感兴趣的开发者。; 使用场景及目标:①学习ELF二进制中字符串加密的典型实现方式与逆向突破口;②掌握从结构识别、函数追踪到算法还原的完整逆向流程;③理解“绑定二进制”的完整性校验设计及其局限性;④实践编写IDAPython脚本自动化提取与解密敏感数据。; 阅读建议:此资源以实战案例驱动,不仅展示技术细节,更强调逆向思维与验证方法,建议读者结合IDA调试环境,逐步跟随文中步骤进行动态分析与算法验证,深入理解每一步的推理依据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值