Chinaunix首页 | 论坛 | 博客
  • 博客访问: 517253
  • 博文数量: 252
  • 博客积分: 6057
  • 博客等级: 准将
  • 技术积分: 1635
  • 用 户 组: 普通用户
  • 注册时间: 2009-12-21 10:17
文章分类

全部博文(252)

文章存档

2013年(1)

2012年(1)

2011年(32)

2010年(212)

2009年(6)

分类:

2010-03-29 10:45:59

作者: LeoVirgo (沙加)
标题: 非本地跳转:应用
时间: 2005年 3月 3日 12:34:43 星期四

第一次写这么长的东西,大家给点意见吧 :)

对于计算机来说,程序只不过是一连串的二进制数据。
逻辑上,程序是这样被执行的:首先,计算机把程序
的内容读取到内存中,并分为三个段:文本段、数据
段和堆栈段。文本段包含了程序中的实际指令,程序
计数器 pc 将被初始化为这个段中的某个地址。数据
段包含程序中静态的和全局的变量。堆栈段用于保存
程序运行时的信息,堆栈指针 sp 被初始化为栈底。
然后,计算机把 pc 指向的地址中的指令读到 CPU 
的寄存器中,执行它,并将 pc 的值作适当的改变使
它指向下一条指令。这个过程被循环直到程序所有的
指令全部被执行。特别地,我们关心函数(或者过程)
调用时发生的事情。

当一个函数被调用时,我们会这样做:首先,将函数
需要的参数压入到堆栈中。压入的顺序并不重要,但
调用者和被调用者要有一致的约定。然后,压入被调
用的函数完成后将要返回的地址。每次压入的过程都
会伴随 sp 的值的改变。最后,将 pc 作适当改变来
使它指向被调用函数的第一条指令。这样,CPU 就将
执行被调用的函数。这时堆栈中可能还会被压入新的
数据,它们是被调用函数中的非静态变量(我称为本
地变量)。执行完成后,我们先把函数准备返回的结
果放入 CPU 的一个寄存器中,然后从堆栈中取出所
有的本地变量,并将这时的栈顶数据(它是函数的返
回地址)写入 pc,再取出所有因为调用这个函数而
压入到堆栈中的数据。这样,执行就可以从调用函数
之前的地方继续。值得说明的是,有时除了参数和返
回地址,一些寄存器的值也要被压入堆栈保存起来以
便恢复。

然而,我们可以修改上面的过程。当调用一个函数时,
我们把要返回的地址和堆栈指针的当前值记录下来,
并在将来的某个时刻用这些保存的值修改 pc 和 sp。
这样,就可以实现所谓的"非本地跳转"。ISO C 中提
供了实现这些操作的函数,它们是 setjmp 和 
longjmp;任何一本 C 的参考手册都解释了这两个函
数。非本地跳转通常用来实现错误的恢复。介绍这个
内容的材料有很多,这里就不赘述了。我们要看看这
个特性的其他两个应用,而且可能会有些变形。前一
个应用是操作系统中的,后一个则是 C++ 和 Java 
对非本地跳转的包装(这继续了非本地跳转的初衷:
错误恢复)。

1. 创建新进程 - fork

在这里,我们先来了解一下进程调度;之后我们将
会知道 fork 这个令人迷惑的系统调用最初是如何
实现的。

在 UNIX 中,进程被定义为程序的可执行映像。在
某一时刻,一台计算机上可以有许多进程在运行,
这些进程共享由操作系统管理的资源。对于每个进
程来说,它都好像在一台计算机上运行,因为操作
系统对它隐藏了不必要的细节。这些隐藏的一部分
是通过分页来实现的。通常计算机都具有地址转换
机制,可以将寄存器中的地址(称为虚地址)映射
为实际的地址(称为物理地址)。在不同的环境下,
同一个虚地址可以映射为不同的物理地址。虚地址
是连续的,比如 0 ~ 4GB(对 Linux 来说),而
与连续的一段虚地址对应的物理地址却不一定是连
续的。我们把所有虚地址的集合称为虚地址空间,
把所有物理地址的集合称为物理地址空间。虚地址
空间可以比物理地址空间大。可以把虚地址空间等
分成许多段,每一段称为一页。进程访问的每一个
地址实际上都是虚地址空间中的一个地址,然后计
算机将会把这个虚地址转换为物理地址。通过这种
机制,对于进程来说,地址就好像是从 0 开始连
续编址的,从而造成了计算机上只有它本身的假象。

我们以早期的 DEC PDP11/40 系统为例,在这个
系统上运行的是 UNIX 的第 6 版内核。PDP11/40 
的内存大小通常是 256KB;它的 CPU 支持两种状
态:内核态和用户态,并且堆栈指针 sp 在这两种
状态下可以具有不同的值。PDP11/40 中虚地址空
间为 0 ~ 64KB-1,被分为 8 页(即 0 ~ 8KB-1,
8KB ~ 16KB-1,......,56KB ~ 64KB-1),并
且内核态和用户态下同一页对应于不同的物理地址。
在内核态下,第 8 页被用于保存设备的寄存器地
址,每个设备的每个寄存器,甚至 CPU 里的寄存
器都可以用这些地址访问到。这样,内核态下实际
上只有 7 页可用,但却可以利用一页的内存访问
所有的设备。在这些寄存器中,有 32 个寄存器用
来保存内存的分页信息。它们被分成两组,每组 16 
个,分别用来保存内核态和用户态的分页信息。每
组的 16 个又被分成 8 个小组,每小组 2 个。
其中一个用来保存与某一页首地址对应的物理地址,
另一个保存了这一页的其他信息(这里并不重要)。

一个进程的文本段具有最低的虚地址,数据段紧随
其后,而堆栈段具有最高的虚地址(为了方便,我
们省略了 bss 段)。同时,可执行文件中包含了
文本段、数据段的大小信息。创建一个进程时,系
统首先读取这些信息,然后根据它们计算与这个进
程对应的分页信息,得到每个段开始的虚地址。同
时,内核保存了一个进程虚地址 0 对应的物理地
址 a0。这样,一旦将一个进程的 a0 与它每个段
(文本段、数据段和堆栈段)起始地址相加的结果
放入用户态的保存分页信息的寄存器并适当设置 pc 
和用户态下的 sp 的值,CPU通过 pc 和 ps 访问
的就是这个进程地址空间内的数据,从而使这个进
程成为当前活动的进程。

为了理解 UNIX v6 内核是如何调度的,先来分析
一下下面三个重要的函数,它们构成了进程创建和
调度的关键。

_savu:
        bis     $340,PS
        mov     (sp)+,r1
        mov     (sp),r0
        mov     sp,(r0)+
        mov     r5,(r0)+
        bic     $340,PS
        jmp     (r1)

_aretu:
        bis     $340,PS
        mov     (sp)+,r1
        mov     (sp),r0
        br      1f

_retu:
        bis     $340,PS
        mov     (sp)+,r1
        mov     (sp),KISA6
        mov     $_u,r0
1:
        mov     (r0)+,sp
        mov     (r0)+,r5
        bic     $340,PS
        jmp     (r1)

这三个函数都是用汇编语言完成的,savu 用来
将 r5 和 sp 这两个寄存器的值保存到其参数指
定的连续内存中;aretu 和 retu 用来恢复 r5 
和 sp 的值。aretu 使用它的参数指定的地址中
的内容。而 retu 使用它的参数修改 KISA6,这
会使内核态下第 7 页(6+1=7)的起始地址发生
变化,从而使与第 7 页对应的物理内存发生变化。
u 是操作系统保存进程信息的结构,它的前两个
字保存了 r5 和 sp。在这里,_u 是一个常量
(140000,或 48KB)。对于每个进程来说,u 
这个结构总是位于内核态下的第 7 页。这样,
retu 就可以恢复每个进程的 r5 和 sp。读者
可能会联想到,在一些情况下,调用 savu 时的
参数就是 u 的地址(140000)。

下面就来看看 swtch 这个调度器是如何工作的。
我们列出它的代码但不做全部解释:

/*
 * This routine is called to reschedule the CPU.
 * if the calling process is not in RUN state,
 * arrangements for it to restart must have
 * been made elsewhere, usually by calling via sleep.
 */
swtch()
{
    static struct proc *p;
    register i, n;
    register struct proc *rp;

    if(p == NULL)
        p = &proc[0];
    /*
     * Remember stack of caller
     */
    savu(u.u_rsav);
    /*
     * Switch to scheduler's stack
     */
    retu(proc[0].p_addr);

loop:
    runrun = 0;
    rp = p;
    p = NULL;
    n = 128;
    /*
     * Search for highest-priority runnable process
     */
    i = NPROC;
    do {
        rp++;
        if(rp >= &proc[NPROC])
            rp = &proc[0];
        if(rp->p_stat==SRUN && (rp->p_flag&SLOAD)!=0) {
            if(rp->p_pri < n) {
                p = rp;
                n = rp->p_pri;
            }
       }
    } while(--i);
    /*
     * If no process is runnable, idle.
     */
    if(p == NULL) {
         p = rp;
         idle();
         goto loop;
    }
    rp = p;
    curpri = n;
    /* Switch to stack of the new process and set up
     * his segmentation registers.
     */
    retu(rp->p_addr);
    sureg();
    /*
     * If the new process paused because it was
     * swapped out, set the stack level to the last call
     * to savu(u_ssav).  This means that the return
     * which is executed immediately after the call to aretu
     * actually returns from the last routine which did
     * the savu.
     *
     * You are not expected to understand this.
     */
    if(rp->p_flag&SSWAP) {
        rp->p_flag =& ~SSWAP;
        aretu(u.u_ssav);
    }
    /* The value returned here has many subtle implications.
     * See the newproc comments.
     */
    return(1);
}

u_rsav 正是 u 的前两个字。我们看到,swtch 
首先保存了调用它的进程的 r5 和 sp(以便将来
恢复),然后恢复了 0 号进程(proc[0])的 r5 
和 sp(proc[0].p_addr 就是 proc[0] 的 u 的
起始地址)。下面的工作是找到另一个进程,它将
被转为活动进程。找到这个进程之后,就可以用 
retu 来恢复它的 r5 和 sp 了。通过调用 sureg,
swtch 设置了将成为活动进程的进程的用户态页面
地址寄存器,这样当 CPU 回到用户态时就可以执行
这个进程的代码了。

在继续之前,让我们回顾一下 swtch 做的工作:
除了找到需要的进程之外,swtch 首先保存了一个
进程的 sp(我们不谈 r5 了);而找到一个进程
之后,swtch 又恢复了它的 sp。这就是实现 fork 
的关键。在这个版本的内核中,fork 是这样写的:

fork()
{
    register struct proc *p1, *p2;

    p1 = u.u_procp;
    for(p2 = &proc[0]; p2 < &proc[NPROC]; p2++)
        if(p2->p_stat == NULL)
            goto found;
    u.u_error = EAGAIN;
    goto out;

found:
    if(newproc()) {
        u.u_ar0[R0] = p1->p_pid;
        u.u_cstime[0] = 0;
        u.u_cstime[1] = 0;
        u.u_stime = 0;
        u.u_cutime[0] = 0;
        u.u_cutime[1] = 0;
        u.u_utime = 0;
        return;
    }
    u.u_ar0[R0] = p2->p_pid;

out:
    u.u_ar0[R7] =+ 2;
}

fork 首先在进程数组 proc 里寻找“空槽”以判
断是否可以创建,然后便调用 newproc 来完成创
建工作。下面是 newproc 的代码:

/*
 * Create a new process-- the internal version of
 * sys fork.
 * It returns 1 in the new process.
 * How this happens is rather hard to understand.
 * The essential fact is that the new process is created
 * in such a way that appears to have started executing
 * in the same call to newproc as the parent;
 * but in fact the code that runs is that of swtch.
 * The subtle implication of the returned value of swtch
 * (see above) is that this is the value that newproc's
 * caller in the new process sees.
 */
newproc()
{
    int a1, a2;
    struct proc *p, *up;
    register struct proc *rpp;
    register *rip, n;

    p = NULL;
    /*
     * First, just locate a slot for a process
     * and copy the useful info from this process into it.
     * The panic "cannot happen" because fork has already
     * checked for the existence of a slot.
     */
retry:
    mpid++;
    if(mpid < 0) {
        mpid = 0;
        goto retry;
    }
    for(rpp = &proc[0]; rpp < &proc[NPROC]; rpp++) {
        if(rpp->p_stat == NULL && p==NULL)
            p = rpp;
        if (rpp->p_pid==mpid)
            goto retry;
    }
    if ((rpp = p)==NULL)
         panic("no procs");

    /*
     * make proc entry for new proc
     */

    rip = u.u_procp;
    up = rip;
    rpp->p_stat = SRUN;
    rpp->p_flag = SLOAD;
    rpp->p_uid = rip->p_uid;
    rpp->p_ttyp = rip->p_ttyp;
    rpp->p_nice = rip->p_nice;
    rpp->p_textp = rip->p_textp;
    rpp->p_pid = mpid;
    rpp->p_ppid = rip->p_pid;
    rpp->p_time = 0;

    /*
     * make duplicate entries
     * where needed
     */

    for(rip = &u.u_ofile[0]; rip < &u.u_ofile[NOFILE];)
        if((rpp = *rip++) != NULL)
            rpp->f_count++;
    if((rpp=up->p_textp) != NULL) {
        rpp->x_count++;
        rpp->x_ccount++;
    }
    u.u_cdir->i_count++;
    /*
     * Partially simulate the environment
     * of the new process so that when it is actually
     * created (by copying) it will look right.
     */
    savu(u.u_rsav);
    rpp = p;
    u.u_procp = rpp;
    rip = up;
    n = rip->p_size;
    a1 = rip->p_addr;
    rpp->p_size = n;
    a2 = malloc(coremap, n);
    /*
     * If there is not enough core for the
     * new process, swap out the current process to generate the
     * copy.
     */
    if(a2 == NULL) {
        rip->p_stat = SIDL;
        rpp->p_addr = a1;
        savu(u.u_ssav);
        xswap(rpp, 0, 0);
        rpp->p_flag =| SSWAP;
        rip->p_stat = SRUN;
    } else {
    /*
     * There is core, so just copy.
     */
        rpp->p_addr = a2;
        while(n--)
            copyseg(a1++, a2++);
    }
    u.u_procp = rip;
    return(0);
}

我们看到,newproc 所作的大部分工作是初始化 
proc 这个数组里的某一项,这是初始化进程的工
作。在所有的初始化完成之后,它便调用 
savu(u.u_rsav) 将 sp 保存起来了。接下来,
newproc 返回 0--这是父进程得到的值。同时,
系统和父进程都没有对子进程做任何操作,子进
程被搁置了。当 swtch 准备使子进程成为活动
进程时,就会先恢复先前由 newproc 保存起来
的堆栈指针 sp,当时的栈顶是调用 newproc 后
应该返回的地址。完成必需的操作之后,swtch 
将返回到那里,不过返回的是 1--这就是子进
程得到的值。

虽然这个 fork 的行为和我们知道的还不一样,
但原理是一样的;非本地跳转在这里起到了关键
作用。

2. C++ 的异常处理机制

作为面向应用的高级语言,C++ 在 C 的基础上
作了许多扩充,最主要的是面向对象的机制。为
了完善这个机制,就需要加入新的异常处理方法。
在 C 中,一部分处理是通过调用 setjmp 和 
longjmp 用非本地跳转来实现的。C++ (和 Java) 
都包装了这个方法,使用 try、throw 和 catch 
来完成类似的功能。实现这个功能有许多方法,
当然可以基于 setjmp 和 longjmp。这里,我们
就用 GCC 中的 C++ 编译器 G++ 的实现来作为
例子。不过需要补充的是,我不会 C++,对于程
序执行的例外情况并不是很清楚,所以我只描述
了程序正常执行时的过程。欢迎读者补充。此外,
由于篇幅限制我只会介绍与非本地跳转有关的内
容,读者还可能需要查看 gcc 的源代码和最后
的参考资料来获得对 C++ 中异常处理的完整认
识。

在 C++ 里,异常的抛出和捕获是需要异常处理
系统的支持的。作为“GNU 编译器集合”的 GCC 
为了简化开发,实现了这个系统。这些代码是通
用的,gnat、g++ 都调用了这个系统中的函数。
要阅读它们,可以下载 GCC 的源代码,然后查
看 gcc 子目录下以 unwind 开头的文件。

我们从下面这段简单的 C++ 程序开始:

namespace Error {
        struct Exception1 { };
        struct Exception2 { };
}

void f(int n)
{
        if (n >= 0)
                throw Error::Exception1();
        else
                throw Error::Exception2();
}

int main(void)
{
        try {
                f(87);
        } catch (Error::Exception1 e) {
        } catch (Error::Exception2 e) {
        }
        return 0;
}

这段程序没有什么功能,但 g++ 编译产生的代
码仍然很复杂。为了展示基于 setjmp 和 longjmp 
的实现,我在 Windows 下用 Cygwin 编译了上
面的程序。下面就是编译的结果,希望没有吓到
读者。

.file "x.cpp"
.text
.align 2
.globl __Z1fi
.def __Z1fi; .scl 2; .type 32; .endef
__Z1fi:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
cmpl $0, 8(%ebp)
js L2
movl $1, (%esp)
call ___cxa_allocate_exception
L3:
movl $0, 8(%esp)
movl $__ZTIN5Error10Exception1E, 4(%esp)
movl %eax, (%esp)
call ___cxa_throw
L2:
movl $1, (%esp)
call ___cxa_allocate_exception
L6:
movl $0, 8(%esp)
movl $__ZTIN5Error10Exception2E, 4(%esp)
movl %eax, (%esp)
call ___cxa_throw
L1:
.def ___main; .scl 2; .type 32; .endef
.def __Unwind_SjLj_Resume; .scl 2; .type 32; .endef
.def ___gxx_personality_sj0; .scl 2; .type 32; .endef
.def __Unwind_SjLj_Register; .scl 2; .type 32; .endef
.def __Unwind_SjLj_Unregister; .scl 2; .type 32; .endef
.align 2
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
subl $92, %esp
andl $-16, %esp
movl $0, %eax
movl %eax, -68(%ebp)
movl -68(%ebp), %eax
call __alloca
movl $___gxx_personality_sj0, -36(%ebp)
movl $LLSDA5, -32(%ebp)
leal -28(%ebp), %eax
leal -12(%ebp), %edx
movl %edx, (%eax)
movl $L17, %edx
movl %edx, 4(%eax)
movl %esp, 8(%eax)
leal -60(%ebp), %eax
movl %eax, (%esp)
call __Unwind_SjLj_Register
call ___main
movl $87, (%esp)
movl $1, -56(%ebp)
call __Z1fi
jmp L9
L17:
leal 12(%ebp), %ebp
movl -52(%ebp), %eax
movl %eax, -72(%ebp)
movl -48(%ebp), %edx
movl %edx, -76(%ebp)
cmpl $2, -76(%ebp)
je L10
cmpl $1, -76(%ebp)
je L13
movl -72(%ebp), %eax
movl %eax, (%esp)
movl $-1, -56(%ebp)
call __Unwind_SjLj_Resume
L10:
movl -72(%ebp), %edx
movl %edx, (%esp)
call ___cxa_begin_catch
L11:
call ___cxa_end_catch
jmp L9
L13:
movl -72(%ebp), %eax
movl %eax, (%esp)
call ___cxa_begin_catch
L14:
call ___cxa_end_catch
L9:
movl $0, -64(%ebp)
L8:
leal -60(%ebp), %eax
movl %eax, (%esp)
call __Unwind_SjLj_Unregister
movl -64(%ebp), %eax
leal -12(%ebp), %esp
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
.section .gcc_except_table,""
.align 4
LLSDA5:
.byte 0xff
.byte 0x0
.uleb128 LLSDATT5-LLSDATTD5
LLSDATTD5:
.byte 0x1
.uleb128 LLSDACSE5-LLSDACSB5
LLSDACSB5:
.uleb128 0x0
.uleb128 0x3
LLSDACSE5:
.byte 0x1
.byte 0x0
.byte 0x2
.byte 0x7d
.align 4
.long __ZTIN5Error10Exception1E
.long __ZTIN5Error10Exception2E
LLSDATT5:
.text
.globl __ZTIN5Error10Exception1E
.section .rdata$_ZTIN5Error10Exception1E,""
.linkonce same_size
.align 4
__ZTIN5Error10Exception1E:
.long __ZTVN10__cxxabiv117__class_type_infoE+8
.long __ZTSN5Error10Exception1E
.globl __ZTIN5Error10Exception2E
.section .rdata$_ZTIN5Error10Exception2E,""
.linkonce same_size
.align 4
__ZTIN5Error10Exception2E:
.long __ZTVN10__cxxabiv117__class_type_infoE+8
.long __ZTSN5Error10Exception2E
.globl __ZTSN5Error10Exception2E
.section .rdata$_ZTSN5Error10Exception2E,""
.linkonce same_size
__ZTSN5Error10Exception2E:
.ascii "N5Error10Exception2E\0"
.globl __ZTSN5Error10Exception1E
.section .rdata$_ZTSN5Error10Exception1E,""
.linkonce same_size
__ZTSN5Error10Exception1E:
.ascii "N5Error10Exception1E\0"
.def ___cxa_end_catch; .scl 3; .type 32; .endef
.def ___cxa_begin_catch; .scl 3; .type 32; .endef
.def ___cxa_throw; .scl 3; .type 32; .endef
.def ___cxa_allocate_exception; .scl 3; .type 32; .endef

首先要说明的是,由于 C++ 的名字破坏机制,
大多数名字都被修改了。我们的 f 被改成了_Z1fi,
Exception1 被改成了 _ZTIN5Error10Exception1E。

注意到函数 main (_main) 在编译后调用了两个函
数:_Unwind_SjLj_Register 和 _Unwind_SjLj_Unregister。
这两个函数属于 gcc 的基于 setjmp 和 longjmp 
的异常处理系统,可以在 gcc 源代码的 gcc/unwind-sjlj.c 
里找到。我们只列出 _Unwind_SjLj_Register 的代码:

void
_Unwind_SjLj_Register (struct SjLj_Function_Context *fc)
{
#if __GTHREADS
  if (use_fc_key < 0)
    fc_key_init_once ();

  if (use_fc_key)
    {
      fc->prev = __gthread_getspecific (fc_key);
      __gthread_setspecific (fc_key, fc);
    }
  else
#endif
    {
      fc->prev = fc_static;
      fc_static = fc;
    }
}

除去宏定义中的代码,函数 _Unwind_Register 
的功能很明显:fc_static 是一个静态的变量,
它保存了进程执行时的堆栈帧的上下文信息。这
是一个单链表,每当调用一个 C++ 函数时,都会
调用 _Unwind_SjLj_Register 向这个链表中添
加一项。调用完成准备从函数返回时,就调用 
_Unwind_SjLj_Unregister 除去链表中的一项。
这些信息将在尝试捕捉异常时有用。

fc_static 是一个指向 SjLj_Function_Context 
结构的指针,而 SjLj_Function_Context 结构
的定义如下:

struct SjLj_Function_Context
{
  /* This is the chain through all registered contexts.  It is
     filled in by _Unwind_SjLj_Register.  */
  struct SjLj_Function_Context *prev;

  /* This is assigned in by the target function before every call
     to the index of the call site in the lsda.  It is assigned by
     the personality routine to the landing pad index.  */
  int call_site;

  /* This is how data is returned from the personality routine to
     the target function's handler.  */
  _Unwind_Word data[4];

  /* These are filled in once by the target function before any
     exceptions are expected to be handled.  */
  _Unwind_Personality_Fn personality;

#ifdef DONT_USE_BUILTIN_SETJMP
  /* We don't know what sort of alignment requirements the system
     jmp_buf has.  We over estimated in except.c, and now we have
     to match that here just in case the system *didn't* have more
     restrictive requirements.  */
  jmp_buf jbuf __attribute__((aligned));
#else
  void *jbuf[];
#endif
};

这里我们关心的只有 prev 和 jbuf 这两个成员。
prev 指向链表的前一个元素,jbuf 保存了与 
ISO C 中的 jmp_buf 相同的内容。

仔细研究抛出异常的动作就可以知道 gcc 的异常
处理系统是如何工作的。注意上面的 f 的汇编代
码(_Z1fi),它先调用 __cxa_allocate_exception 
生成了一个异常对象,然后初始化这个对象(这里
是 _ZTIN5Error10Exception1E,如果对象是一个
类则会调用它的初始化方法),最后调用 __cxa_throw。

__cxa_throw 的代码位于 libstdc++-v3/libsupc++/eh_throw.cc:

extern "C" void
__cxa_throw (void *obj, std::type_info *tinfo, void (*dest) (void *))
{
  __cxa_exception *header = __get_exception_header_from_obj (obj);
  header->exceptionType = tinfo;
  header->exceptionDestructor = dest;
  header->unexpectedHandler = __unexpected_handler;
  header->terminateHandler = __terminate_handler;
  header->unwindHeader.exception_class = __gxx_exception_class;
  header->unwindHeader.exception_cleanup = __gxx_exception_cleanup;

  __cxa_eh_globals *globals = __cxa_get_globals ();
  globals->uncaughtExceptions += 1;

#ifdef _GLIBCXX_SJLJ_EXCEPTIONS
  _Unwind_SjLj_RaiseException (&header->unwindHeader);
#else
  _Unwind_RaiseException (&header->unwindHeader);
#endif

  // Some sort of unwinding error.  Note that terminate is a handler.
  __cxa_begin_catch (&header->unwindHeader);
  std::terminate ();
}

可以看到,__cxa_throw 先为异常对象中的某些项
赋了值使它可以使用,然后就调用 
_Unwind_SjLj_RaiseException 或 _Unwind_RaiseException 
来抛出和捕获一个异常。如果执行正常,这两个函数
都不返回,直接执行源程序中与 catch 对应的代码。
_Uwind_RaiseException 和 _Unwind_SjLj_RaiseException 
实际上是一个函数,只是使用了宏定义的方式而已。
_Unwind_RaiseException 在 gcc/unwind.inc 里。
这里我只做简单的说明,完整的说明可以在最后的参
考资料中找到。

总的来说,抛出和捕获异常分为两个阶段:

- 在查找阶段,异常处理系统会尝试找到可以处理被
抛出异常的处理者。利用每次调用函数时建立的链表,
系统会找到每次函数调用时的堆栈。如果在那里找到
了处理者,就进入第二个阶段。

- 在清理阶段,系统每次都会向回跳跃一个栈帧,并
执行可能的清理工作。当跳跃到包含处理者的栈帧时,
系统就恢复寄存器的状态,控制就跳回到由用户写的
处理代码了。

从 _Unwind_RaiseException 的代码中我们可以看
到 gcc 的实现:

_Unwind_Reason_Code
_Unwind_RaiseException(struct _Unwind_Exception *exc)
{
  struct _Unwind_Context this_context, cur_context;
  _Unwind_Reason_Code code;

  /* Set up this_context to describe the current stack frame.  */
  uw_init_context (&this_context);
  cur_context = this_context;

  /* Phase 1: Search.  Unwind the stack, calling the personality routine
     with the _UA_SEARCH_PHASE flag set.  Do not modify the stack yet.  */
  while (1)
    {
      _Unwind_FrameState fs;

      /* Set up fs to describe the FDE for the caller of cur_context.  The
         first time through the loop, that means __cxa_throw.  */
      code = uw_frame_state_for (&cur_context, &fs);

      if (code == _URC_END_OF_STACK)
        /* Hit end of stack with no handler found.  */
        return _URC_END_OF_STACK;

      if (code != _URC_NO_REASON)
        /* Some error encountered.  Ususally the unwinder doesn't
           diagnose these and merely crashes.  */
        return _URC_FATAL_PHASE1_ERROR;

      /* Unwind successful.  Run the personality routine, if any.  */
      if (fs.personality)
        {
          code = (*fs.personality) (1, _UA_SEARCH_PHASE,
exc->exception_class,
                                    exc, &cur_context);
          if (code == _URC_HANDLER_FOUND)
            break;
          else if (code != _URC_CONTINUE_UNWIND)
            return _URC_FATAL_PHASE1_ERROR;
        }

      /* Update cur_context to describe the same frame as fs.  */
      uw_update_context (&cur_context, &fs);
    }

  /* Indicate to _Unwind_Resume and associated subroutines that this
     is not a forced unwind.  Further, note where we found a handler.  */
  exc->private_1 = 0;
  exc->private_2 = uw_identify_context (&cur_context);

  cur_context = this_context;
  code = _Unwind_RaiseException_Phase2 (exc, &cur_context);
  if (code != _URC_INSTALL_CONTEXT)
    return code;

  uw_install_context (&this_context, &cur_context);
}

uw_install_context 简单地返回当前的上下文信息 
(fc_static)。对 personality 函数的调用则会查
找处理者。最后, uw_update_context 使栈帧向上
跳跃一次。如果查找成功,_Unwind_RaiseException_Phase2 
就会返回 _URC_INSTALL_CONTEXT,而调用 uw_install_context 
就会跳跃到用户写的异常处理代码:

#define uw_install_context(CURRENT, TARGET)           \
do                                                    \
  {                                                   \
    _Unwind_SjLj_SetContext ((TARGET)->fc);           \
    longjmp ((TARGET)->fc->jbuf, 1);                  \
  }                                                   \
while (0)

现在回到我们最初的程序编译出来的汇编代码。一旦
捕获了一个异常,就会执行 .L17 中的代码。这里有
一个明显的比较动作:

        movl    %edx, -76(%ebp)
        cmpl    $2, -76(%ebp)
        je      L10
        cmpl    $1, -76(%ebp)
        je      L13
        movl    -72(%ebp), %eax
        movl    %eax, (%esp)
        movl    $-1, -56(%ebp)
        call    __Unwind_SjLj_Resume

1 和 2 是 g++ 为 Exception1 和 Exception2 产
生的不同的标识,通过它,就可以对捕获到的 Exception1 
类型的和 Exception2 类型的异常作出不同的处理。
此外,如果没有可以处理的例程,就调用 
_Unwind_SjLj_Resume,而 _Unwind_SjLj_Resume 
会调用 std::terminate 来使程序异常终止。

至于正常执行的路径,汇编代码非常清楚,这里就不
说明了。

我们来看看 C++ ABI for Itanium: Exception Handling 
举出的 C++ 异常实现的例子,它可以加深对 g++ 
产生的汇编代码的理解。下面的 try-catch 块:

try { foo(); }
catch (TYPE1) { ... }
catch (TYPE2) { buz(); }
bar();

可以翻译成这样:

// In "Normal" area:
foo(); // Call Attributes: Landing Pad L1, Action Record A1
goto E1;
...
E1: // End Label
bar();

// In "Exception" area;
L1: // Landing Pad label
[Back-end generated ompensationcode]
goto C1;

C1: // Cleanup label
[Front-end generated cleanup code, destructors, etc]
[corresponding to exit of try { } block]
goto S1;

S1: // Switch label
switch(SWITCH_VALUE_PAD_ARGUMENT)
{
    case 1: goto H1; // For TYPE1
    case 2: goto H2; // For TYPE2
    //...
    default: goto X1;
}

X1:
[Cleanup code corresponding to exit of scope]
[enclosing the try block]
_Unwind_Resume();

H1: // Handler label
[Initialize catch parameter]
__cxa_begin_catch(exception);
[User code]
goto R1;

H2:
[Initialize catch parameter]
__cxa_begin_catch(exception);
[User code]
buz(); // Call attributes: Landing pad L2, action record A2
goto R1;

R1: // Resume label:
__cxa_end_catch();
goto E1;

L2:
C2:
// Make sure we cleanup the current exception
__cxa_end_catch();

X2:
[Cleanup code corresponding to exit of scope]
[enclosing the try block]
_Unwind_Resume();

如果读者用 -static 来编译程序并反汇编得到的可执
行文件,将会发现凡是 C++ 例程都会在进入以后先调
用 _Unwind_Register,并在返回之前调用 _Unwind_Unregister。
这样,就保证异常处理系统可以正常工作。这也就是 C 
编译器和 C++ 编译器的不同之处。为了方便应用层程
序员编写程序,C++ 确实在背后做了许多工作。而异常
处理这一部分,是可以用非本地跳转的方式来实现的。

上面我们看到了应用非本地跳转的两个例子。这是一个
非常底层的功能:我们需要理解计算机如何执行程序才
能用上这个功能,并且要使用它们必须使用汇编语言。
但是,它也因此而具有强大的功能,这个功能被聪明的
人利用以后成为了许多灾难的来源。大多数程序的漏洞
──缓冲区溢出──也是因为返回地址被修改,这也算
是非本地跳转的一个鲜明的例子吧。

参考资料

我对应用非本地跳转的理解来自 UNIX 的源代码。有一
本非常不错的书,John Lions 的 Commentary on UNIX 
6th Edition with Source Code,介绍了第 6 版 UNIX 
内核。虽然它已经很老了,但它包含了最初的结构,后
来的许多改进都没有摆脱它的影响;此外,通过阅读 
UNIX v6 的源代码,一个人可以在很短的时间内对一
个完整的操作系统有比较深入的理解。

关于 C++ 的异常处理,我的理解来自 C++ ABI for 
Itanium: Exception Handling 
(
)。
我不会 C++,对 C++ 的资料也不熟悉,但希望这个网
址对 C++ 的爱好者有帮助。

另一本书是 Randal E. Bryant 和 David O'Hallaron 
的 Computer Systems: A Programmer's Perspective。
它有中文版,由龚奕利、雷迎春翻译,名为深入理解计算
机系统。这本书对计算机系统做了详细和完备的介绍,
值得一看。

最后,自由软件带来的开放源代码运动是最好的教室。
要更深入地理解 gcc 的异常处理系统,可以看一下 gcc 
源代码中 gcc 子目录里 unwind 开头的文件和 
libstdc++-v3/libsupc++ 子目录下 eh 开头的文件。
阅读(1283) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

chulia200020012010-03-29 12:51:20

LCC编绎器 http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=74740&page=&view=&sb=&o=&fpart=2&vc=1 不知大家有没有用过LCC-Win32,它是一个完全由个人编写的微型WIN32集成开发环境.它是在普通LCC编绎器基础上,作了大量的改进完成的.我使用中发现它的C编绎器对代码的优化非常干净利落,在某些方面甚至超过了大名鼎鼎的GCC.GCC在生成指令时,经常脱泥带水.作者在他的技术手册 lccwin32.doc中,对LCC的编绎原理和优化方法作了详尽的剖析,还介绍了自已在开发中的各种心得体会,对C编绎器感兴趣的朋友不可不看. 下载地址 http://www.cs.virginia.edu/~lcc-win32/ ftp://ftp.cs.virginia.edu/pub/lcc-win32/lccdoc.exe ftp://ftp.cs.virginia.edu/pub/lcc-win32/lccwin32.exe