CSAPP:第 8 章 异常控制流

发布于 16 天前  27 次阅读


本篇文章是对 CSAPP 第 8 章异常控制流的复现记录,部分代码有笔者的更改

Linux 系统调用

int main() {
    write(1, "hello, world\n", 13);
    _exit(0);
}

image-20230116111658839

进程控制

获取进程 ID

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

int main () {
    printf("pid = %d\n", getpid());
    printf("ppid = %d\n", getppid());
    return 0;
}

image-20230116114045527

创建和终止进程

#include "csapp.h"

/* $begin fork */
/* $begin wasidefork */
int main() 
{
    pid_t pid;
    int x = 1;

    pid = Fork(); //line:ecf:forkreturn
    if (pid == 0) {  /* Child */
    printf("child : x=%d\n", ++x); //line:ecf:childprint
    exit(0);
    }

    /* Parent */
    printf("parent: x=%d\n", --x); //line:ecf:parentprint
    exit(0);
}
/* $end fork */
/* $end wasidefork */


image-20230116114655624

#include "csapp.h"

int main() {
    Fork();
    Fork();
    printf("hello\n");
    return 0;
}

image-20230116115130579

回收子进程

image-20230116121428263

/* $begin waitpid2 */
#include "csapp.h"
#define N 2

int main() 
{
    int status, i;
    pid_t pid[N], retpid;

    /* Parent creates N children */
    for (i = 0; i < N; i++) 
    if ((pid[i] = Fork()) == 0)  /* Child */          //line:ecf:waitpid2:fork
        exit(100+i);

    /* Parent reaps N children in order */
    i = 0;
    while ((retpid = waitpid(pid[i++], &status, 0)) > 0) { //line:ecf:waitpid2:waitpid
    if (WIFEXITED(status))  
        printf("child %d terminated normally with exit status=%d\n",
           retpid, WEXITSTATUS(status));
    else
        printf("child %d terminated abnormally\n", retpid);
    }

    /* The only normal termination is if there are no more children */
    if (errno != ECHILD) 
    unix_error("waitpid error");

    exit(0);
}
/* $end waitpid2 */

image-20230116121520522

#include "csapp.h"

/* $begin waitprob1 */
/* $begin wasidewaitprob1 */
int main() 
{
    int status;
    pid_t pid;

    printf("Hello\n");
    pid = Fork();
    printf("%d\n", !pid);
    if (pid != 0) {
    if (waitpid(-1, &status, 0) > 0) {
        if (WIFEXITED(status) != 0)
        printf("WEXITSTATUS: %d\n", WEXITSTATUS(status));
    }
    }
    printf("Bye\n");
    exit(2);
}
/* $end waitprob1 */
/* $end wasidewaitprob1 */

image-20230116122500049

让进程休眠

unsigned int snooze(unsigned int secs) {
    unsigned int rc = sleep(secs);

    printf("Slept for %d of %d secs.\n", secs-rc, secs);
    return rc;
}

image-20230116125303481

加载并运行程序

#include "csapp.h"

int main(int argc, char *argv[], char *envp[]) {
    int i;
    printf("Command-line arguments:\n");
    for(i = 0; argv[i] != NULL; ++i)
        printf("    argc[%2d]: %s\n", i, argv[i]);
    printf("\n");
    printf("Environment variables: \n");
    for(i = 0; envp[i] != NULL; ++i)
        printf("    envp[%2d]: %s\n", i, envp[i]);
    return 0;
}

image-20230116130800403

利用 fork 和 execve 运行程序

code/ecf/shellx.c 实现了一个简单的 shell,这里略去代码

image-20230116132939376

信号

发送信号

/* $begin kill */
#include "csapp.h"

int main() 
{
    pid_t pid;

    /* Child sleeps until SIGKILL signal received, then dies */   
    if ((pid = Fork()) == 0) {   
    Pause();  /* Wait for a signal to arrive */  
    printf("control should never reach here!\n");
    exit(0);
    }

    /* Parent sends a SIGKILL signal to a child */
    Kill(pid, SIGKILL);
    exit(0);
}
/* $end kill */

上面的示例代码没有明显现象,就不放图了

接收信号

/* $begin sigint */
#include "csapp.h"

void sigint_handler(int sig) /* SIGINT handler */   //line:ecf:sigint:beginhandler
{
    printf("Caught SIGINT!\n");    //line:ecf:sigint:printhandler
    exit(0);                      //line:ecf:sigint:exithandler
}                                              //line:ecf:sigint:endhandler

int main() 
{
    /* Install the SIGINT handler */         
    if (signal(SIGINT, sigint_handler) == SIG_ERR)  //line:ecf:sigint:begininstall
    unix_error("signal error");                 //line:ecf:sigint:endinstall

    pause(); /* Wait for the receipt of a signal */  //line:ecf:sigint:pause

    return 0;
}
/* $end sigint */

image-20230116135310310

既然我们已经修改了默认的 sigint_handler,为了让收到 SIGINT 信号程序不会终止,我尝试把 exit(0) 改成 while(1)

void sigint_handler(int sig) /* SIGINT handler */   //line:ecf:sigint:beginhandler
{
    printf("\rCaught SIGINT!\n");    //line:ecf:sigint:printhandler
    while(1);
    // exit(0);                      //line:ecf:sigint:exithandler
}    

image-20230116140158041

再尝试改成 while(1) pause(); 的时候,效果实现了:

image-20230116140526753

练习题 8.7

/* $begin snoozesignal */
#include "csapp.h"

/* SIGINT handler */
void handler(int sig) 
{
    return; /* Catch the signal and return */
}

/* $begin snooze */
unsigned int snooze(unsigned int secs) {
    unsigned int rc = sleep(secs);

    printf("\rSlept for %d of %d secs.\n", secs-rc, secs);
    return rc;
}
/* $end snooze */

int main(int argc, char **argv) {

    if (argc != 2) {
    fprintf(stderr, "usage: %s <secs>\n", argv[0]);
    exit(0);
    }

    if (signal(SIGINT, handler) == SIG_ERR) /* Install SIGINT handler */
    unix_error("signal error\n");
    (void)snooze(atoi(argv[1]));
    exit(0);
}
/* $end snoozesignal */

image-20230116141035358

编写信号处理程序

signal1.c,存在 bug,它假设信号是会排队的

image-20230116143850880

signal2.c,正确的信号处理

image-20230116144045198

修改了下 signal2.c,看看是不是第一次触发 handler2 的时候就能回收完所有僵死进程

image-20230116144914277

确实如此,代码如下

#include "csapp.h"

/* $begin signal2 */
void handler2(int sig) 
{
    static int cnt = 0;
    int olderrno = errno;

    while (waitpid(-1, NULL, 0) > 0) {
        Sio_puts("Handler reaped child\n");
    }
    if (errno != ECHILD)
        Sio_error("waitpid error");

    Sio_puts("Called times: ");
    Sio_putl(++cnt);
    Sio_puts("\n");

    Sleep(1);
    errno = olderrno;
}
/* $end signal2 */

int main() 
{
    int i, n;
    char buf[MAXBUF];

    if (signal(SIGCHLD, handler2) == SIG_ERR)
        unix_error("signal error");

    /* Parent creates children */
    for (i = 0; i < 3; i++) {
        if (Fork() == 0) {
            printf("Hello from child %d\n", (int)getpid());
            exit(0);
        }
    }

    /* Parent waits for terminal input and then processes it */
    if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
        unix_error("read");

    printf("Parent processing input\n");
    while (1)
        ;

    exit(0);
}

练习题 8.8

image-20230116160944733

用 sigprocmask 来同步进程

在这个例子中,父进程保证在相应的 deletejob 之前执行 addjob

#include "csapp.h"

void initjobs()
{
}

void addjob(int pid)
{
}

void deletejob(int pid)
{
}

/* $begin procmask2 */
void handler(int sig)
{
    int olderrno = errno;
    sigset_t mask_all, prev_all;
    pid_t pid;

    Sigfillset(&mask_all);
    while ((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap a zombie child */
        Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        deletejob(pid); /* Delete the child from the job list */
        Sigprocmask(SIG_SETMASK, &prev_all, NULL);
    }
    if (errno != ECHILD)
        Sio_error("waitpid error");
    errno = olderrno;
}

int main(int argc, char **argv)
{
    int pid;
    sigset_t mask_all, mask_one, prev_one;

    Sigfillset(&mask_all);
    Sigemptyset(&mask_one);
    Sigaddset(&mask_one, SIGCHLD);
    Signal(SIGCHLD, handler);
    initjobs(); /* Initialize the job list */

    while (1) {
        Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* Block SIGCHLD */
        if ((pid = Fork()) == 0) { /* Child process */
            Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
            Execve("/bin/date", argv, NULL);
        }
        Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */  
        addjob(pid);  /* Add the child to the job list */
        Sigprocmask(SIG_SETMASK, &prev_one, NULL);  /* Unblock SIGCHLD */
    }
    exit(0);
}
/* $end procmask2 */

显示等待信号

使用 sigsuspend 挂起该进程,sigsuspend 等价于下述代码的原子的(不可中断的)版本:

sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

以下是完整代码实现:

/* $begin sigsuspend */
#include "csapp.h"

volatile sig_atomic_t pid;

void sigchld_handler(int s)
{
    int olderrno = errno;
    pid = Waitpid(-1, NULL, 0);
    errno = olderrno;
}

void sigint_handler(int s)
{
}

int main(int argc, char **argv) 
{
    sigset_t mask, prev;

    Signal(SIGCHLD, sigchld_handler);
    Signal(SIGINT, sigint_handler);
    Sigemptyset(&mask);
    Sigaddset(&mask, SIGCHLD);

    while (1) {
        Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
        if (Fork() == 0) /* Child */
            exit(0);

        /* Wait for SIGCHLD to be received */
        pid = 0;
        while (!pid) 
            Sigsuspend(&prev);

        /* Optionally unblock SIGCHLD */
        Sigprocmask(SIG_SETMASK, &prev, NULL); 

        /* Do some work after receiving SIGCHLD */
        printf(".");
    }
    exit(0);
}
/* $end sigsuspend */

非本地跳转

出于某种超出本书描述范围的原因,setjmp 返回的值不能被赋值给变量,不过它可以安全地作用在 swtich 或条件语句的测试中

setjmp 只被调用一次,但返回多次(第一次调用时返回 0后面可能多次从 longjmp 返回,返回值非 0,从下图可以发现即使 VAL = 0 也会变成 1)

image-20230116202826029

非本地跳转的一个重要应用就是允许从一个深层嵌套的函数调用中立即返回,我们可以使用非本地跳转直接返回到一个普通的本地化的错误处理程序,而不是费力地解开调用栈

/* $begin setjmp */
#include "csapp.h"

jmp_buf buf;

int error1 = 0; 
int error2 = 1;

void foo(void), bar(void);

int main() 
{
    switch(setjmp(buf)) {
        case 0: 
        printf("case = 0\n");
        foo();
            break;
        case 1: 
        printf("Detected an error1 condition in foo\n");
            break;
        case 2: 
        printf("Detected an error2 condition in foo\n");
            break;
        default:
        printf("Unknown error condition in foo\n");
    }
    exit(0);
}

/* Deeply nested function foo */
void foo(void) 
{
    if (error1)
        longjmp(buf, 1); 
    bar();
}

void bar(void) 
{
    if (error2)
        longjmp(buf, 2); 
}
/* $end setjmp */

image-20230116203417018

sigsetjmpsiglongjmp 函数是 setjmplongjmp 的可以被信号处理程序使用的版本。 sigsetjmp 多了一个参数 savemask ,如果非 0 则会保存 signal mask

image-20230116204827843

通过学习了下 C 语言插入内嵌汇编的格式,我修改了一下 restart.c,达到了如下效果:

image-20230116213638984

cnt 是保存在栈上的,而 r15 则是对应的寄存器 %r15,通过多次尝试我发现,这里所谓的保存当前环境,只会保存寄存器,不会保存栈。(我曾尝试过把 cnt 前加 register 修饰,或者加上 -O2 优化,但每次恢复后 cnt 的值总是没有恢复)

/* $begin restart */
#include "csapp.h"

sigjmp_buf buf;

void handler(int sig) 
{
    siglongjmp(buf, 1);
}

int main() 
{
    int cnt = 0, r15;
    asm volatile (
        "movq $0, %r15"
    );
    if (!sigsetjmp(buf, 1)) {
        Signal(SIGINT, handler);
        Sio_puts("starting\n");
    } else 
        Sio_puts("restarting\n");

    while(1) {
        Sleep(1);
        Sio_puts("cnt = ");
        Sio_putl(++cnt);
        asm volatile (
            "inc %%r15 \n"
            "movl %%r15d, %0 \n"
            :"=r"(r15)
            :
            :
        );
        Sio_puts("  r15 = ");
        Sio_putl(r15);
        Sio_puts("  processing...\n");
    }
    exit(0); /* Control never reaches here */
}
/* $end restart */

C语言的内嵌汇编

GCC x86 内嵌汇编

C/C++ 中嵌入汇编总结

https://www.xianwaizhiyin.net/?p=1104

https://cloud.tencent.com/developer/article/1166330

https://blog.csdn.net/zjy900507/article/details/79487605

以上是 C 语言内嵌汇编的相关参考博客,感觉没有一个写得比较全写得好的,查个格式都要看半天😓


昨日的月光悄然退场,曦阳已经渐亮