diff options
author | Juergen Lock <nox@FreeBSD.org> | 2009-05-30 19:50:09 +0000 |
---|---|---|
committer | Juergen Lock <nox@FreeBSD.org> | 2009-05-30 19:50:09 +0000 |
commit | 0cbfdbee46004362a104238baa166134d7f6b06a (patch) | |
tree | 9b9920e47b3b5a5258023adf8b42b2eb52348e57 /emulators/kqemu-kmod-devel | |
parent | 675686ab8b49a8a8f5ad591a998b3143dab453af (diff) | |
download | ports-0cbfdbee46004362a104238baa166134d7f6b06a.tar.gz ports-0cbfdbee46004362a104238baa166134d7f6b06a.zip |
Notes
Diffstat (limited to 'emulators/kqemu-kmod-devel')
7 files changed, 538 insertions, 2 deletions
diff --git a/emulators/kqemu-kmod-devel/Makefile b/emulators/kqemu-kmod-devel/Makefile index 96af32721578..8f9873a3dcdb 100644 --- a/emulators/kqemu-kmod-devel/Makefile +++ b/emulators/kqemu-kmod-devel/Makefile @@ -7,7 +7,7 @@ PORTNAME= kqemu PORTVERSION= 1.4.0.p1 -PORTREVISION= 2 +PORTREVISION= 3 CATEGORIES= emulators kld MASTER_SITES= http://bellard.org/qemu/ \ http://qemu.org/ \ @@ -48,7 +48,10 @@ KMODDIR= /boot/modules MAKE_ENV+= KMODDIR="${KMODDIR}" -.if !exists(${SRC_BASE}/sys/Makefile) +SYSDIR?= ${SRC_BASE}/sys +MAKE_ENV+= SYSDIR="${SYSDIR}" + +.if !exists(${SYSDIR}/Makefile) IGNORE= requires kernel source to be installed .endif @@ -74,6 +77,15 @@ post-patch: .if ${OSVERSION} >= 701100 && (${OSVERSION} < 800000 || ${OSVERSION} >= 800046) @cd ${WRKSRC} && ${PATCH} -R --quiet < ${FILESDIR}/patch-tssworkaround .endif +# Apply extra patches from the qemu-devel list; in the unlikely case that +# these cause regressions please define WITHOUT_EXTRA_PATCHES and post +# details about your host _and_ guest on freebsd-emulation@freebsd.org +# - Thanx! +.if !defined(WITHOUT_EXTRA_PATCHES) + for i in ${FILESDIR}/extra-patch-*; do \ + ${PATCH} -d ${WRKSRC} --quiet <$$i;\ + done +.endif do-build: @(cd ${BUILD_WRKSRC}/common; ${SETENV} ${MAKE_ENV} ${GMAKE} ${MAKE_FLAGS} Makefile ${MAKE_ARGS} ${ALL_TARGET}) diff --git a/emulators/kqemu-kmod-devel/files/extra-patch-ea-200711-netbsd b/emulators/kqemu-kmod-devel/files/extra-patch-ea-200711-netbsd new file mode 100644 index 000000000000..ea1c410c44d2 --- /dev/null +++ b/emulators/kqemu-kmod-devel/files/extra-patch-ea-200711-netbsd @@ -0,0 +1,70 @@ +From: 3n4ch3@gmail.com (Enache Adrian) +Subject: [Qemu-devel] [kqemu patch] get Open/NetBSD to work with the kqemu + accelerator +Date: Mon, 5 Nov 2007 22:00:12 +0200 +Message-ID: <281a8e2bec2c45bce8b89bf96457995c.fa6dd951@aerenchyma> +To: qemu-devel@nongnu.org + +[sorry if this is the wrong list, but I haven't figured out any public + address where I could send kqemu bug reports and patches] + +Currently, both NetBSD and OpenBSD are hanging or crashing when running +on qemu with the kqemu accelerator enabled. + +This happens because both systems are using a weird scheme where they +are loading the GDT table with LGDT up-front (with the limit set to +the maximum), but are growing the table and actually mapping the memory +behind it only when needed. +(see src/sys/arch/i386/i386/gdt.c in both source trees) + +That is causing the kqemu accelerator to generate a page fault in +update_dt_cache() when trying to fill its 'soft' tlb with pages that +are beyond the real end of the GDT table. + +With this diff applied, NetBSD and OpenBSD seem to run fine with +kqemu + user-only virtualization (I've tried netbsd-4.0-rc2 and +openbsd 4.2). + +Full virtualization (-kernel-kqemu) doesn't work yet for different +reasons (I think). + +Regards, +Adi + +Index: common/monitor.c +--- xx/kqemu-1.3.0pre11/common/monitor.c Tue Feb 6 23:02:00 2007 ++++ kqemu-1.3.0pre11/common/monitor.c Mon Nov 5 18:59:58 2007 +@@ -990,7 +990,8 @@ static void *map_vaddr(struct kqemu_state *s, unsigned + e = &s->soft_tlb[(addr >> PAGE_SHIFT) & (SOFT_TLB_SIZE - 1)]; + redo: + if (e->vaddr[(is_user << 1) + is_write] != (addr & PAGE_MASK)) { +- soft_tlb_fill(s, addr, is_write, is_user); ++ if(cpu_x86_handle_mmu_fault(s, addr, is_write, is_user, 1)) ++ return NULL; + goto redo; + } else { + taddr = e->addend + addr; +@@ -1802,6 +1803,11 @@ static void update_dt_cache(struct kqemu_state *s, int + page_end = dt_end; + sel2 = sel + (page_end - dt_ptr); + ptr = map_vaddr(s, dt_ptr, 0, 0); ++ if(!ptr) ++ /* Open/NetBSD have a 'dynamic' GDT, but they load the gdt ++ register with LGDT only once and with a limit far beyond ++ the end of the memory actually mapped for the table */ ++ goto skip_the_rest; + ram_addr = ram_ptr_to_ram_addr(s, ptr); + if (dt_changed || + s->dt_ram_addr[dt_type][pindex] != ram_addr || +@@ -1818,7 +1824,7 @@ static void update_dt_cache(struct kqemu_state *s, int + sel_end = (s->dt_limit[dt_type] + 1) & ~7; + if (sel < sel_end) + reset_dt_entries(s, dt_type, sel, sel_end); +- ++skip_the_rest: + s->dt_base[dt_type] = base; + s->dt_limit[dt_type] = limit; + } + + + diff --git a/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-01 b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-01 new file mode 100644 index 000000000000..7102987fde6d --- /dev/null +++ b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-01 @@ -0,0 +1,51 @@ +From: jan.kiszka@siemens.com (Jan Kiszka) +Subject: [Qemu-devel] [PATCH 1/5] kqemu: x86-64: Run 16-bit-stack-return in + ring 0 +Date: Fri, 29 May 2009 19:18:31 +0200 +Message-ID: <20090529171831.14265.89986.stgit@mchn012c.ww002.siemens.net> +To: qemu-devel@nongnu.org + +The fix-up code for returning to 16 bit stack segments is currently +executed in ring 1. This has the side effect that the interrupt flag +remains cleared on return from ring 1 to the guest's ring 3 as its +IOPL is 0 (ie. iret can only modify IF when run in ring 0). + +Fix this by moving the 32 bit return trampoline into ring 0. + +Signed-off-by: Jan Kiszka <jan.kiszka@siemens.com> +--- + + common/kernel.c | 8 ++++---- + 1 files changed, 4 insertions(+), 4 deletions(-) + +diff --git a/common/kernel.c b/common/kernel.c +index 6a88452..1d7a69e 100644 +Index: common/kernel.c +--- a/common/kernel.c ++++ b/common/kernel.c +@@ -422,8 +422,8 @@ struct kqemu_state *kqemu_init(struct kqemu_init *d, + #ifdef __x86_64__ + s->monitor_ds_sel = 0; /* no need for a specific data segment */ + /* used for 16 bit esp fix */ +- s->monitor_cs32_sel = (s->monitor_selector_base + (7 << 3)) | 1; +- s->monitor_ss16_sel = (s->monitor_selector_base + (6 << 3)) | 1; ++ s->monitor_cs32_sel = s->monitor_selector_base + (7 << 3); ++ s->monitor_ss16_sel = s->monitor_selector_base + (6 << 3); + s->monitor_ss_null_sel = (s->monitor_selector_base + (1 << 3)) | 3; + #else + s->monitor_ds_sel = s->monitor_selector_base + (1 << 3); +@@ -495,9 +495,9 @@ struct kqemu_state *kqemu_init(struct kqemu_init *d, + 0, 0xfffff, 0xa09a); /* long mode segment */ + set_seg((uint32_t *)(dt_table + (s->monitor_ss16_sel >> 3)), + (s->monitor_data_vaddr + offsetof(struct kqemu_state, stack)) & ~0xffff, +- 0xffff, 0x00b2); /* SS16 segment for 16 bit ESP fix */ ++ 0xffff, 0x0092); /* SS16 segment for 16 bit ESP fix */ + set_seg((uint32_t *)(dt_table + (s->monitor_cs32_sel >> 3)), +- 0, 0xfffff, 0xc0ba); /* CS32 segment for 16 bit ESP fix */ ++ 0, 0xfffff, 0xc09a); /* CS32 segment for 16 bit ESP fix */ + set_seg((uint32_t *)(dt_table + (s->monitor_ss_null_sel >> 3)), + 0, 0, 0x40f2); /* substitute for null SS segment */ + #else + + + diff --git a/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-02 b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-02 new file mode 100644 index 000000000000..f9aca1d181e9 --- /dev/null +++ b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-02 @@ -0,0 +1,102 @@ +From: jan.kiszka@siemens.com (Jan Kiszka) +Subject: [Qemu-devel] [PATCH 2/5] kqemu: i386: Reorder DS and ES on + exception stack +Date: Fri, 29 May 2009 19:18:31 +0200 +Message-ID: <20090529171831.14265.74474.stgit@mchn012c.ww002.siemens.net> +To: qemu-devel@nongnu.org + +This is a KQEMU upstream bug: In case the non-trivial paths of +LOAD_SEG_CACHE in exception_return are taken for both DS and ES, the +current code will break as it assumes to read from the monitor's DS in +the restore code for ES. Fix this by swapping both segments on the stack +so that ES is always restored before DS. + +Signed-off-by: Jan Kiszka <jan.kiszka@siemens.com> +--- + + common/i386/monitor_asm.S | 16 ++++++++-------- + common/kqemu_int.h | 6 +++--- + 2 files changed, 11 insertions(+), 11 deletions(-) + +diff --git a/common/i386/monitor_asm.S b/common/i386/monitor_asm.S +index 04f4258..e996553 100644 +Index: common/i386/monitor_asm.S +--- a/common/i386/monitor_asm.S ++++ b/common/i386/monitor_asm.S +@@ -214,8 +214,8 @@ __monitor_exception: + pushl %edx + pushl %ecx + pushl %eax +- pushl %es + pushl %ds ++ pushl %es + + /* compute the address of the monitor context */ + call 1f +@@ -260,17 +260,17 @@ exception_return: + cmpb $3, KQEMU_STATE_cpu_state_cpl(%ebx) + je normal_seg_load + popl %eax +- LOAD_SEG_CACHE(%ds, R_DS, (11 * 4)) ++ LOAD_SEG_CACHE(%es, R_ES, (11 * 4)) + popl %eax +- LOAD_SEG_CACHE(%es, R_ES, (10 * 4)) ++ LOAD_SEG_CACHE(%ds, R_DS, (10 * 4)) + jmp 2f + normal_seg_load: + #endif + 1: +- popl %ds ++ popl %es + SEG_EXCEPTION(1b) + 1: +- popl %es ++ popl %ds + SEG_EXCEPTION(1b) + 2: + +@@ -295,10 +295,10 @@ SEG_EXCEPTION(1b) + + exception_return_to_monitor: + 1: +- popl %ds ++ popl %es + SEG_EXCEPTION(1b) + 1: +- popl %es ++ popl %ds + SEG_EXCEPTION(1b) + popl %eax + popl %ecx +@@ -363,8 +363,8 @@ __monitor_interrupt: + pushl %edx + pushl %ecx + pushl %eax +- pushl %es + pushl %ds ++ pushl %es + + /* compute the address of the monitor context */ + call 1f +diff --git a/common/kqemu_int.h b/common/kqemu_int.h +index f19f7ca..4b59cb3 100644 +Index: common/kqemu_int.h +--- a/common/kqemu_int.h ++++ b/common/kqemu_int.h +@@ -367,10 +367,10 @@ struct kqemu_exception_regs { + }; + #else + struct kqemu_exception_regs { +- uint16_t ds_sel; /* 0 */ +- uint16_t ds_sel_h; +- uint16_t es_sel; /* 1 */ ++ uint16_t es_sel; /* 0 */ + uint16_t es_sel_h; ++ uint16_t ds_sel; /* 1 */ ++ uint16_t ds_sel_h; + uint32_t eax; /* 2 */ + uint32_t ecx; + uint32_t edx; + + + diff --git a/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-03 b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-03 new file mode 100644 index 000000000000..f1e7b92d76e2 --- /dev/null +++ b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-03 @@ -0,0 +1,38 @@ +From: jan.kiszka@siemens.com (Jan Kiszka) +Subject: [Qemu-devel] [PATCH 3/5] kqemu: Fix forbidden selector range change +Date: Fri, 29 May 2009 19:18:31 +0200 +Message-ID: <20090529171831.14265.66049.stgit@mchn012c.ww002.siemens.net> +To: qemu-devel@nongnu.org + +Do not bail out on LDT selectors that match the reserved monitor GDT +selector range. At this chance, improve the related panic message. + +Signed-off-by: Jan Kiszka <jan.kiszka@siemens.com> +--- + + common/interp.c | 7 +++++-- + 1 files changed, 5 insertions(+), 2 deletions(-) + +diff --git a/common/interp.c b/common/interp.c +index 088d2b2..4c042e9 100644 +Index: common/interp.c +--- a/common/interp.c ++++ b/common/interp.c +@@ -775,9 +775,12 @@ static void load_seg_desc(struct kqemu_state *s, + #ifdef DEBUG_SEG + monitor_log(s, "load_seg_desc: reg=%d sel=0x%04x\n", seg_reg, selector); + #endif +- if (selector >= s->monitor_selector_base && ++ if (!(selector & 0x4) && selector >= s->monitor_selector_base && + selector <= (s->monitor_selector_base + MONITOR_SEL_RANGE)) { +- monitor_panic(s, "Trying to load a reserved selector\n"); ++ monitor_panic(s, "Trying to load a reserved selector " ++ "(reg=%d sel=0x%04x cs:ip=%04x:" FMT_lx ")\n", ++ seg_reg, selector, env->segs[R_CS].selector, ++ (unsigned long)env->eip); + } + + if ((selector & 0xfffc) == 0) { + + + diff --git a/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-04 b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-04 new file mode 100644 index 000000000000..57ab6d9f5cfd --- /dev/null +++ b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-04 @@ -0,0 +1,108 @@ +From: jan.kiszka@siemens.com (Jan Kiszka) +Subject: [Qemu-devel] [PATCH 4/5] kqemu: Implement verr/verw in the monitor + code interpreter +Date: Fri, 29 May 2009 19:18:31 +0200 +Message-ID: <20090529171831.14265.57241.stgit@mchn012c.ww002.siemens.net> +To: qemu-devel@nongnu.org + +This avoids user space for handling verr/verw via TCG. + +Signed-off-by: Jan Kiszka <jan.kiszka@siemens.com> +--- + + common/interp.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- + 1 files changed, 70 insertions(+), 1 deletions(-) + +diff --git a/common/interp.c b/common/interp.c +index 4c042e9..4f93bc3 100644 +Index: common/interp.c +--- a/common/interp.c ++++ b/common/interp.c +@@ -1720,6 +1720,65 @@ void helper_lldt(struct kqemu_state *s, int selector) + env->ldt.selector = selector; + } + ++static void helper_verr(struct kqemu_state *s, int selector) ++{ ++ uint32_t e1, e2; ++ int rpl, dpl, cpl; ++ ++ if ((selector & 0xfffc) == 0) ++ goto fail; ++ if (load_segment(s, &e1, &e2, selector) != 0) ++ goto fail; ++ if (!(e2 & DESC_S_MASK)) ++ goto fail; ++ rpl = selector & 3; ++ dpl = (e2 >> DESC_DPL_SHIFT) & 3; ++ cpl = s->cpu_state.cpl; ++ if (e2 & DESC_CS_MASK) { ++ if (!(e2 & DESC_R_MASK)) ++ goto fail; ++ if (!(e2 & DESC_C_MASK)) { ++ if (dpl < cpl || dpl < rpl) ++ goto fail; ++ } ++ } else { ++ if (dpl < cpl || dpl < rpl) { ++ fail: ++ set_reset_eflags(s, 0, CC_Z); ++ return; ++ } ++ } ++ set_reset_eflags(s, CC_Z, 0); ++} ++ ++static void helper_verw(struct kqemu_state *s, int selector) ++{ ++ uint32_t e1, e2; ++ int rpl, dpl, cpl; ++ ++ if ((selector & 0xfffc) == 0) ++ goto fail; ++ if (load_segment(s, &e1, &e2, selector) != 0) ++ goto fail; ++ if (!(e2 & DESC_S_MASK)) ++ goto fail; ++ rpl = selector & 3; ++ dpl = (e2 >> DESC_DPL_SHIFT) & 3; ++ cpl = s->cpu_state.cpl; ++ if (e2 & DESC_CS_MASK) { ++ goto fail; ++ } else { ++ if (dpl < cpl || dpl < rpl) ++ goto fail; ++ if (!(e2 & DESC_W_MASK)) { ++ fail: ++ set_reset_eflags(s, 0, CC_Z); ++ return; ++ } ++ } ++ set_reset_eflags(s, CC_Z, 0); ++} ++ + static void helper_wrmsr(struct kqemu_state *s) + { + #ifdef __x86_64__ +@@ -4479,7 +4538,17 @@ QO( case OT_LONG | 8:\ + case 5: /* verw */ + if (!(s->cpu_state.cr0 & CR0_PE_MASK) || get_eflags_vm(s)) + goto illegal_op; +- raise_exception(s, KQEMU_RET_SOFTMMU); ++ if (mod == 3) { ++ rm = (modrm & 7) | REX_B(s); ++ val = get_regS(s, OT_WORD, rm) & 0xffff; ++ } else { ++ addr = get_modrm(s, modrm); ++ val = ldS(s, OT_WORD, addr); ++ } ++ if (op == 4) ++ helper_verr(s, val); ++ else ++ helper_verw(s, val); + break; + default: + goto illegal_op; + + + diff --git a/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-05 b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-05 new file mode 100644 index 000000000000..23b2d17c9822 --- /dev/null +++ b/emulators/kqemu-kmod-devel/files/extra-patch-jk-200905-05 @@ -0,0 +1,155 @@ +From: jan.kiszka@siemens.com (Jan Kiszka) +Subject: [Qemu-devel] [PATCH 5/5] kqemu: Implement lar/lsl in the monitor + code interpreter +Date: Fri, 29 May 2009 19:18:31 +0200 +Message-ID: <20090529171831.14265.17606.stgit@mchn012c.ww002.siemens.net> +To: qemu-devel@nongnu.org + +This avoids user space for handling lar/lsl via TCG. + +Signed-off-by: Jan Kiszka <jan.kiszka@siemens.com> +--- + + common/interp.c | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- + 1 files changed, 109 insertions(+), 2 deletions(-) + +diff --git a/common/interp.c b/common/interp.c +index 4f93bc3..577d666 100644 +Index: common/interp.c +--- a/common/interp.c ++++ b/common/interp.c +@@ -1720,6 +1720,93 @@ void helper_lldt(struct kqemu_state *s, int selector) + env->ldt.selector = selector; + } + ++static int helper_lar(struct kqemu_state *s, int selector) ++{ ++ uint32_t e1, e2; ++ int rpl, dpl, cpl, type; ++ ++ if ((selector & 0xfffc) == 0) ++ goto fail; ++ if (load_segment(s, &e1, &e2, selector) != 0) ++ goto fail; ++ rpl = selector & 3; ++ dpl = (e2 >> DESC_DPL_SHIFT) & 3; ++ cpl = s->cpu_state.cpl; ++ if (e2 & DESC_S_MASK) { ++ if ((e2 & DESC_CS_MASK) && (e2 & DESC_C_MASK)) { ++ /* conforming */ ++ } else { ++ if (dpl < cpl || dpl < rpl) ++ goto fail; ++ } ++ } else { ++ type = (e2 >> DESC_TYPE_SHIFT) & 0xf; ++ switch(type) { ++ case 1: ++ case 2: ++ case 3: ++ case 4: ++ case 5: ++ case 9: ++ case 11: ++ case 12: ++ break; ++ default: ++ goto fail; ++ } ++ if (dpl < cpl || dpl < rpl) { ++ fail: ++ set_reset_eflags(s, 0, CC_Z); ++ return 0; ++ } ++ } ++ set_reset_eflags(s, CC_Z, 0); ++ return e2 & 0x00f0ff00; ++} ++ ++static int helper_lsl(struct kqemu_state *s, int selector) ++{ ++ unsigned int limit; ++ uint32_t e1, e2; ++ int rpl, dpl, cpl, type; ++ ++ if ((selector & 0xfffc) == 0) ++ goto fail; ++ if (load_segment(s, &e1, &e2, selector) != 0) ++ goto fail; ++ rpl = selector & 3; ++ dpl = (e2 >> DESC_DPL_SHIFT) & 3; ++ cpl = s->cpu_state.cpl; ++ if (e2 & DESC_S_MASK) { ++ if ((e2 & DESC_CS_MASK) && (e2 & DESC_C_MASK)) { ++ /* conforming */ ++ } else { ++ if (dpl < cpl || dpl < rpl) ++ goto fail; ++ } ++ } else { ++ type = (e2 >> DESC_TYPE_SHIFT) & 0xf; ++ switch(type) { ++ case 1: ++ case 2: ++ case 3: ++ case 9: ++ case 11: ++ break; ++ default: ++ goto fail; ++ } ++ if (dpl < cpl || dpl < rpl) { ++ fail: ++ set_reset_eflags(s, 0, CC_Z); ++ return 0; ++ } ++ } ++ limit = get_seg_limit(e1, e2); ++ set_reset_eflags(s, CC_Z, 0); ++ return limit; ++} ++ + static void helper_verr(struct kqemu_state *s, int selector) + { + uint32_t e1, e2; +@@ -4616,6 +4703,28 @@ QO( case OT_LONG | 8:\ + goto illegal_op; + } + goto insn_next; ++ LABEL(102) /* lar */ ++ LABEL(103) /* lsl */ ++ if (!(s->cpu_state.cr0 & CR0_PE_MASK) || get_eflags_vm(s)) ++ goto illegal_op; ++ ot = s->dflag + OT_WORD; ++ modrm = ldub_code(s); ++ mod = (modrm >> 6); ++ if (mod == 3) { ++ rm = (modrm & 7) | REX_B(s); ++ val = get_regS(s, OT_WORD, rm) & 0xffff; ++ } else { ++ addr = get_modrm(s, modrm); ++ val = ldS(s, OT_WORD, addr); ++ } ++ rm = ((modrm >> 3) & 7) | REX_R(s); ++ if (b == 0x102) ++ val = helper_lar(s, val); ++ else ++ val = helper_lsl(s, val); ++ if (s->regs1.eflags & CC_Z) ++ set_regS(s, ot, rm, val); ++ goto insn_next; + LABEL(108) /* invd */ + LABEL(109) /* wbinvd */ + if (s->cpu_state.cpl != 0) +@@ -5214,8 +5323,6 @@ QO( case OT_LONG | 8:\ + LABEL(10b) + LABEL(10a) + LABEL(104) +- LABEL(103) +- LABEL(102) + LABEL(f1) + LABEL(e2) + LABEL(e1) + + + |