《深入理解计算机系统》第八章 (二)程序的加载与运行

本文深入探讨了一个基于fork和execve实现的简单外壳程序,解释了其如何处理命令输入、执行可执行文件以及处理内置命令。同时,文章还详细阐述了加载过程,包括加载器如何将可执行文件内容加载到内存并运行。最后,文章提供了关于32位Linux系统中内存映像和用户栈组织结构的解释。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

/* $begin shellmain */
#include "csapp.h"
#define MAXARGS   128

/* function prototypes */
void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argv);

int main()
{
    char cmdline[MAXLINE]; /* Command line */

    while (1) {
    /* Read */
    printf("> ");                   
    fgets(cmdline, MAXLINE, stdin);
    if (feof(stdin))
        exit(0);

    /* Evaluate */
    eval(cmdline);
    }
}
/* $end shellmain */
 
/* $begin eval */
/* eval - Evaluate a command line */
void eval(char *cmdline)
{
    char *argv[MAXARGS]; /* Argument list execve() */
    char buf[MAXLINE];   /* Holds modified command line */
    int bg;              /* Should the job run in bg or fg? */
    pid_t pid;           /* Process id */
    
    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    /*printf("%d\n",bg);*/
    if (argv[0] == NULL)  
    return;   /* Ignore empty lines */

    if (!builtin_command(argv)) {
    if ((pid = fork()) == 0) {   /* Child runs user job */
        if (execve(argv[0], argv, environ) < 0) {
        printf("%s: Command not found.\n", argv[0]);
        printf("fork error: %s\n",strerror(errno));
        exit(0);
        }
    }

    /* Parent waits for foreground job to terminate */
    if (!bg) {
        int status;
        if (waitpid(pid, &status, 0) < 0)
        /*unix_error("waitfg: waitpid error");*/
        fprintf(stderr,"fork error: %s\n",strerror(errno));
        exit(0);
    }
    else
        printf("%d %s", pid, cmdline);
    }
    return;
}

/* If first arg is a builtin command, run it and return true */
int builtin_command(char **argv)
{
    if (!strcmp(argv[0], "quit")) /* quit command */
    exit(0);  
    if (!strcmp(argv[0], "&"))    /* Ignore singleton & */
    return 1;
    return 0;                     /* Not a builtin command */
}
/* $end eval */

/* $begin parseline */
/* parseline - Parse the command line and build the argv array */
int parseline(char *buf, char **argv)
{
    char *delim;         /* Points to first space delimiter */
    int argc;            /* Number of args */
    int bg;              /* Background job? */

    buf[strlen(buf)-1] = ' ';  /* Replace trailing '\n' with space */
    while (*buf && (*buf == ' ')) /* Ignore leading spaces */
    buf++;

    /* Build the argv list */
    argc = 0;
    while ((delim = strchr(buf, ' '))) {
    argv[argc++] = buf;
    *delim = '\0';
    buf = delim + 1;
    while (*buf && (*buf == ' ')) /* Ignore spaces */
           buf++;
    }
    argv[argc] = NULL;
    
    if (argc == 0)  /* Ignore blank line */
    return 1;

    /* Should the job run in the background? */
    if ((bg = (*argv[argc-1] == '&')) != 0)
    argv[--argc] = NULL;

    return bg;
}

/* $end parseline */


上面的代码是书中提到的通过fork和execve实现的一个简单的外壳。运行情况如下:

Fighting!hust_smartcar@:~/work/cs_code /try$./shellex
> hello sanwu
Hello sanwu!
Fighting!hust_smartcar@:~/work/cs_code /try$./shellex
> quit
其中quit是内置命令。对于内置命令,外壳进程会马上解释这个命令。对于可执行文件,如hello,外壳父进程会在一个新的子进程的上下文中加载并运行这个文件。


运行可执行目标文件p,因为p不是一个内置的外壳命令,所以外壳会通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码运行p。加载器可以通过execve函数进行调用。加载器将可执行目标文件中的代码和数据从磁盘中拷贝到存储器中,然后通过跳转到程序的第一条指令或者入口点来运行程序。将程序拷贝到存储器并运行的过程叫做加载。

在32位的linux系统中,代码段从0x08048000处开始。存储器映像如上图所示。其中用户栈在运行时创建,总是从合法用户地址开始,向下增长(向低存储器地址方向增长)。从栈的上部开始的段是为操作系统驻留的存储器的部分(内核)的代码和数据保留的。

用户栈的组织结构如下如所示:

当加载器运行时,创建存储器映像,在可执行文件中段头部表的指导下,加载器将可执行文件的相关内容拷贝到代码段和数据段,接下来,加载器跳转到程序的入口点,也就是字符_start的地址,在_start地址处的启动代码是在目标文件ctrl.o中定义的,对所有的C程序都一样。

0x080480c0 <_start>          /*Entry point in .text*/

          call _ _libc_init_first  /*Startup code in .text*/

          call _init                    /*Startup code in .init*/

          call atexit                  /*Startup code in .text*/

          call main                   /*Application main routine*/

          call _exit                   /*Returns control to OS*/

(注:上面没有显示将每个函数的参数压栈的代码)

在.text和.init节中调用了初始化里称号,启动代码调用atexit例程,这个程序附加了一系列在应用程序正常中止时应该调用的程序。exit函数运行atexit注册的函数,然后通过_exit将控制返回给操作系统。接着,启动代码调用应用程序的main程序,开始执行我们的C代码。应用程序返回之后,启动代码调用_exit程序,将控制返回给操作系统。



### 关于《深入理解计算机系统第八章实验的内容 #### 实验概述 《深入理解计算机系统第八章主要围绕操作系统的核心概念展开,重点讨论了进程管理、内存管理和文件系统的实现机制。本章节的实验通常涉及以下几个方面: 1. **进程创建控制** 使用 `fork()` 和 `execve()` 函数来创建子进程并加载新的可执行程序[^1]。 2. **异常处理系统调用** 学习如何通过系统调用触发异常,并了解其分类(中断、陷阱、故障和终止)。特别是陷阱的作用在于提供用户空间程序内核之间的交互接口[^2]。 3. **进程标识符操作** 掌握获取当前进程 ID (`getpid()`) 及其父进程 ID (`getppid()`) 的方法[^3]。 --- #### 示例代码解析 以下是基于第八章内容的一个典型实验案例——使用 `execve` 加载外部命令 `/bin/ls` 并打印错误信息: ```c #include "csapp.h" int main(int argc, const char *argv[], const char *envp[]) { if (execve("/bin/ls", argv, envp) < 0) { // 尝试加载 /bin/ls 命令 printf("/bin/ls not found.\n"); return -1; } return 0; } ``` 此代码片段展示了如何利用 `execve` 替换当前进程映像为指定路径下的新程序。如果目标程序不存在,则会进入条件分支输出提示信息[^1]。 --- #### 系统调用异常处理详解 在现代操作系统中,异常是一种重要的控制流转移方式。当处理器检测到特定事件时,它会依据预设的异常向量表跳转至对应处理函数。例如,在用户态发起一条 `syscall` 指令即可触发展现形式之一——陷阱[^2]。 这种设计允许应用程序安全地请求服务而无需直接访问硬件资源。常见的场景包括但不限于打开文件、分配内存以及网络通信等基础功能支持。 --- #### 获取进程ID的实际应用 为了更好地理解和调试多线程或多进程环境下的行为模式,《深入理解计算机系统》还介绍了两个关键 API —— `getpid()` 和 `getppid()` 。它们分别用于查询当前运行实例及其上级启动者的唯一编号。 下面给出一段简单的演示代码: ```c #include <stdio.h> #include <unistd.h> int main() { pid_t current_pid = getpid(); // 调用 getpid() pid_t parent_pid = getppid(); // 调用 getppid() printf("Current Process ID: %d\nParent Process ID: %d\n", current_pid, parent_pid); return 0; } ``` 以上例子清晰表明了父子关系链路结构对于构建复杂软件架构的重要性。 --- ### 总结 通过对上述知识点的学习可以加深读者对底层原理的认识程度;同时也能培养动手实践能力从而达到理论联系实际的目的。希望这些材料能够帮助您顺利完成课程安排的任务! ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值