move eclair from ihk repository
This commit is contained in:
42
configure.ac
42
configure.ac
@ -36,6 +36,12 @@ AC_ARG_ENABLE([dcfa],
|
|||||||
[AS_HELP_STRING(
|
[AS_HELP_STRING(
|
||||||
[--enable-dcfa],[Enable DCFA modules])],[],[enable_dcfa=no])
|
[--enable-dcfa],[Enable DCFA modules])],[],[enable_dcfa=no])
|
||||||
|
|
||||||
|
AC_ARG_ENABLE([memdump],
|
||||||
|
AC_HELP_STRING([--enable-memdump],
|
||||||
|
[enable dumping memory and analyzing a dump]),
|
||||||
|
[ENABLE_MEMDUMP=$enableval],
|
||||||
|
[ENABLE_MEMDUMP=default])
|
||||||
|
|
||||||
case "X$WITH_KERNELSRC" in
|
case "X$WITH_KERNELSRC" in
|
||||||
Xyes | Xno | X)
|
Xyes | Xno | X)
|
||||||
WITH_KERNELSRC='/lib/modules/`uname -r`/build'
|
WITH_KERNELSRC='/lib/modules/`uname -r`/build'
|
||||||
@ -209,6 +215,41 @@ MCCTRL_FIND_KSYM([sys_mount])
|
|||||||
MCCTRL_FIND_KSYM([sys_unshare])
|
MCCTRL_FIND_KSYM([sys_unshare])
|
||||||
MCCTRL_FIND_KSYM([zap_page_range])
|
MCCTRL_FIND_KSYM([zap_page_range])
|
||||||
|
|
||||||
|
case $ENABLE_MEMDUMP in
|
||||||
|
yes|no|auto)
|
||||||
|
;;
|
||||||
|
default)
|
||||||
|
if test "x$WITH_TARGET" = "xsmp-x86" ; then
|
||||||
|
ENABLE_MEMDUMP=auto
|
||||||
|
else
|
||||||
|
ENABLE_MEMDUMP=no
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
AC_MSG_ERROR([unknown memdump argument: $ENABLE_MEMDUMP])
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if test "x$ENABLE_MEMDUMP" != "xno" ; then
|
||||||
|
enableval=yes
|
||||||
|
AC_CHECK_LIB([bfd],[bfd_init],[],[enableval=no])
|
||||||
|
AC_CHECK_HEADER([bfd.h],[],[enableval=no])
|
||||||
|
|
||||||
|
if test "x$ENABLE_MEMDUMP" = "xyes" -a "x$enableval" = "xno" ; then
|
||||||
|
AC_MSG_ERROR([memdump feature needs bfd.h and libbfd a.k.a bunutils-devel])
|
||||||
|
fi
|
||||||
|
ENABLE_MEMDUMP=$enableval
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test "x$ENABLE_MEMDUMP" = "xyes" ; then
|
||||||
|
AC_MSG_NOTICE([memdump feature is enabled])
|
||||||
|
AC_DEFINE([ENABLE_MEMDUMP],[1],[whether memdump feature is enabled])
|
||||||
|
uncomment_if_ENABLE_MEMDUMP=''
|
||||||
|
else
|
||||||
|
AC_MSG_NOTICE([memdump feature is disabled])
|
||||||
|
uncomment_if_ENABLE_MEMDUMP='#'
|
||||||
|
fi
|
||||||
|
|
||||||
AC_SUBST(CC)
|
AC_SUBST(CC)
|
||||||
AC_SUBST(XCC)
|
AC_SUBST(XCC)
|
||||||
AC_SUBST(ARCH)
|
AC_SUBST(ARCH)
|
||||||
@ -226,6 +267,7 @@ AC_SUBST(DCFA_VERSION)
|
|||||||
AC_SUBST(IHK_RELEASE_DATE)
|
AC_SUBST(IHK_RELEASE_DATE)
|
||||||
AC_SUBST(MCKERNEL_RELEASE_DATE)
|
AC_SUBST(MCKERNEL_RELEASE_DATE)
|
||||||
AC_SUBST(DCFA_RESEASE_DATE)
|
AC_SUBST(DCFA_RESEASE_DATE)
|
||||||
|
AC_SUBST(uncomment_if_ENABLE_MEMDUMP)
|
||||||
|
|
||||||
AC_CONFIG_HEADERS([executer/config.h])
|
AC_CONFIG_HEADERS([executer/config.h])
|
||||||
AC_CONFIG_FILES([
|
AC_CONFIG_FILES([
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
CC=@CC@
|
CC=@CC@
|
||||||
BINDIR=@BINDIR@
|
BINDIR=@BINDIR@
|
||||||
KDIR ?= @KDIR@
|
KDIR ?= @KDIR@
|
||||||
CFLAGS=-Wall -O -fPIE -pie
|
CFLAGS=-Wall -O
|
||||||
VPATH=@abs_srcdir@
|
VPATH=@abs_srcdir@
|
||||||
TARGET=mcexec
|
TARGET=mcexec
|
||||||
|
@uncomment_if_ENABLE_MEMDUMP@TARGET+=eclair
|
||||||
|
LIBS=@LIBS@
|
||||||
|
|
||||||
all: $(TARGET)
|
all: $(TARGET)
|
||||||
|
|
||||||
mcexec: mcexec.c
|
mcexec: mcexec.c
|
||||||
$(CC) -I${KDIR} $(CFLAGS) $(EXTRA_CFLAGS) -lrt -pthread -o $@ $^ $(EXTRA_OBJS)
|
$(CC) -I${KDIR} $(CFLAGS) $(EXTRA_CFLAGS) -fPIE -pie -lrt -pthread -o $@ $^ $(EXTRA_OBJS)
|
||||||
|
|
||||||
|
eclair: eclair.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LIBS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(RM) $(TARGET) *.o
|
$(RM) $(TARGET) *.o
|
||||||
@ -18,4 +23,5 @@ clean:
|
|||||||
install:
|
install:
|
||||||
mkdir -p -m 755 $(BINDIR)
|
mkdir -p -m 755 $(BINDIR)
|
||||||
install -m 755 mcexec $(BINDIR)
|
install -m 755 mcexec $(BINDIR)
|
||||||
|
@uncomment_if_ENABLE_MEMDUMP@install -m 755 eclair $(BINDIR)
|
||||||
|
|
||||||
|
|||||||
966
executer/user/eclair.c
Normal file
966
executer/user/eclair.c
Normal file
@ -0,0 +1,966 @@
|
|||||||
|
/**
|
||||||
|
* \file eclair.c
|
||||||
|
* License details are found in the file LICENSE.
|
||||||
|
* \brief
|
||||||
|
* IHK os memory dump analyzer for McKernel
|
||||||
|
* \author Gou Nakamura <go.nakamura.yw@hitachi-solutions.com> \par
|
||||||
|
* Copyright (C) 2015 RIKEN AICS
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <bfd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
#define CPU_TID_BASE 1000000
|
||||||
|
|
||||||
|
struct options {
|
||||||
|
uint8_t cpu;
|
||||||
|
uint8_t help;
|
||||||
|
char *kernel_path;
|
||||||
|
char *dump_path;
|
||||||
|
char *log_path;
|
||||||
|
}; /* struct options */
|
||||||
|
|
||||||
|
struct thread_info {
|
||||||
|
struct thread_info *next;
|
||||||
|
int status;
|
||||||
|
#define PS_RUNNING 0x01
|
||||||
|
#define PS_INTERRUPTIBLE 0x02
|
||||||
|
#define PS_UNINTERRUPTIBLE 0x04
|
||||||
|
#define PS_STOPPED 0x20
|
||||||
|
#define PS_TRACED 0x40
|
||||||
|
#define CS_IDLE 0x010000
|
||||||
|
#define CS_RUNNING 0x020000
|
||||||
|
#define CS_RESERVED 0x030000
|
||||||
|
int pid;
|
||||||
|
int tid;
|
||||||
|
int cpu;
|
||||||
|
int lcpu;
|
||||||
|
int padding;
|
||||||
|
uintptr_t process;
|
||||||
|
uintptr_t clv;
|
||||||
|
uintptr_t x86_clv;
|
||||||
|
}; /* struct thread_info */
|
||||||
|
|
||||||
|
static struct options opt;
|
||||||
|
static volatile int f_done = 0;
|
||||||
|
static bfd *symbfd = NULL;
|
||||||
|
static bfd *dumpbfd = NULL;
|
||||||
|
static asection *dumpscn = NULL;
|
||||||
|
static int num_processors = -1;
|
||||||
|
static asymbol **symtab = NULL;
|
||||||
|
static ssize_t nsyms;
|
||||||
|
static uintptr_t kernel_base;
|
||||||
|
static struct thread_info *tihead = NULL;
|
||||||
|
static struct thread_info **titailp = &tihead;
|
||||||
|
static struct thread_info *curr_thread = NULL;
|
||||||
|
static uintptr_t ihk_mc_switch_context = -1;
|
||||||
|
|
||||||
|
static uintptr_t lookup_symbol(char *name) {
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < nsyms; ++i) {
|
||||||
|
if (!strcmp(symtab[i]->name, name)) {
|
||||||
|
return (symtab[i]->section->vma + symtab[i]->value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#define NOSYMBOL ((uintptr_t)-1)
|
||||||
|
return NOSYMBOL;
|
||||||
|
} /* lookup_symbol() */
|
||||||
|
|
||||||
|
static uintptr_t virt_to_phys(uintptr_t va) {
|
||||||
|
#define MAP_KERNEL 0xFFFFFFFF80000000
|
||||||
|
if (va >= MAP_KERNEL) {
|
||||||
|
return (va - MAP_KERNEL + kernel_base);
|
||||||
|
}
|
||||||
|
#define MAP_ST 0xFFFF800000000000
|
||||||
|
if (va >= MAP_ST) {
|
||||||
|
return (va - MAP_ST);
|
||||||
|
}
|
||||||
|
if (0) printf("virt_to_phys(%lx): -1\n", va);
|
||||||
|
#define NOPHYS ((uintptr_t)-1)
|
||||||
|
return NOPHYS;
|
||||||
|
} /* virt_to_phys() */
|
||||||
|
|
||||||
|
static int read_physmem(uintptr_t pa, void *buf, size_t size) {
|
||||||
|
off_t off;
|
||||||
|
bfd_boolean ok;
|
||||||
|
|
||||||
|
if (pa < dumpscn->vma) {
|
||||||
|
printf("read_physmem(%lx,%p,%lx):too small pa. vma %lx\n", pa, buf, size, dumpscn->vma);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
off = pa - dumpscn->vma;
|
||||||
|
if (off >= dumpscn->size) {
|
||||||
|
printf("read_physmem(%lx,%p,%lx):too large pa. vma %lx size %lx\n", pa, buf, size, dumpscn->vma, dumpscn->size);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ((dumpscn->size - off) < size) {
|
||||||
|
printf("read_physmem(%lx,%p,%lx):too large size. vma %lx size %lx\n", pa, buf, size, dumpscn->vma, dumpscn->size);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
ok = bfd_get_section_contents(dumpbfd, dumpscn, buf, off, size);
|
||||||
|
if (!ok) {
|
||||||
|
bfd_perror("read_physmem:bfd_get_section_contents");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} /* read_physmem() */
|
||||||
|
|
||||||
|
static int read_mem(uintptr_t va, void *buf, size_t size) {
|
||||||
|
uintptr_t pa;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
pa = virt_to_phys(va);
|
||||||
|
if (pa == NOPHYS) {
|
||||||
|
if (0) {
|
||||||
|
/* NOPHYS is usual for 'bt' command */
|
||||||
|
perror("read_mem:virt_to_phys");
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
error = read_physmem(pa, buf, size);
|
||||||
|
if (error) {
|
||||||
|
perror("read_mem:read_physmem");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} /* read_mem() */
|
||||||
|
|
||||||
|
static int read_64(uintptr_t va, void *buf) {
|
||||||
|
return read_mem(va, buf, sizeof(uint64_t));
|
||||||
|
} /* read_64() */
|
||||||
|
|
||||||
|
static int read_32(uintptr_t va, void *buf) {
|
||||||
|
return read_mem(va, buf, sizeof(uint32_t));
|
||||||
|
} /* read_32() */
|
||||||
|
|
||||||
|
static int read_symbol_64(char *name, void *buf) {
|
||||||
|
uintptr_t va;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
va = lookup_symbol(name);
|
||||||
|
if (va == NOSYMBOL) {
|
||||||
|
printf("read_symbol_64(%s):lookup_symbol failed\n", name);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = read_64(va, buf);
|
||||||
|
if (error) {
|
||||||
|
printf("read_symbol_64(%s):read_64(%#lx) failed", name, va);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} /* read_symbol_64() */
|
||||||
|
|
||||||
|
enum {
|
||||||
|
/* cpu_local_var */
|
||||||
|
CPU_LOCAL_VAR_SIZE = 0,
|
||||||
|
CURRENT_OFFSET,
|
||||||
|
RUNQ_OFFSET,
|
||||||
|
CPU_STATUS_OFFSET,
|
||||||
|
|
||||||
|
/* process */
|
||||||
|
CTX_OFFSET,
|
||||||
|
SCHED_LIST_OFFSET,
|
||||||
|
PROC_OFFSET,
|
||||||
|
|
||||||
|
/* fork_tree_node */
|
||||||
|
STATUS_OFFSET,
|
||||||
|
PID_OFFSET,
|
||||||
|
TID_OFFSET,
|
||||||
|
|
||||||
|
END_MARK,
|
||||||
|
}; /* enum */
|
||||||
|
static uintptr_t debug_constants[END_MARK+1];
|
||||||
|
#define K(name) (debug_constants[name])
|
||||||
|
|
||||||
|
static int setup_constants(void) {
|
||||||
|
int error;
|
||||||
|
uintptr_t va;
|
||||||
|
|
||||||
|
va = lookup_symbol("debug_constants");
|
||||||
|
if (va == NOSYMBOL) {
|
||||||
|
perror("debug_constants");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = read_mem(va, debug_constants, sizeof(debug_constants));
|
||||||
|
if (error) {
|
||||||
|
perror("debug_constants");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0) {
|
||||||
|
printf("CPU_LOCAL_VAR_SIZE: %ld\n", K(CPU_LOCAL_VAR_SIZE));
|
||||||
|
printf("CURRENT_OFFSET: %ld\n", K(CURRENT_OFFSET));
|
||||||
|
printf("RUNQ_OFFSET: %ld\n", K(RUNQ_OFFSET));
|
||||||
|
printf("CPU_STATUS_OFFSET: %ld\n", K(CPU_STATUS_OFFSET));
|
||||||
|
printf("CTX_OFFSET: %ld\n", K(CTX_OFFSET));
|
||||||
|
printf("SCHED_LIST_OFFSET: %ld\n", K(SCHED_LIST_OFFSET));
|
||||||
|
printf("PROC_OFFSET: %ld\n", K(PROC_OFFSET));
|
||||||
|
printf("STATUS_OFFSET: %ld\n", K(STATUS_OFFSET));
|
||||||
|
printf("PID_OFFSET: %ld\n", K(PID_OFFSET));
|
||||||
|
printf("TID_OFFSET: %ld\n", K(TID_OFFSET));
|
||||||
|
printf("END_MARK: %ld\n", K(END_MARK));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} /* setup_constants() */
|
||||||
|
|
||||||
|
static int setup_threads(void) {
|
||||||
|
int error;
|
||||||
|
uintptr_t clv;
|
||||||
|
int cpu;
|
||||||
|
uintptr_t current;
|
||||||
|
uintptr_t locals;
|
||||||
|
size_t locals_span;
|
||||||
|
|
||||||
|
error = read_symbol_64("num_processors", &num_processors);
|
||||||
|
if (error) {
|
||||||
|
perror("num_processors");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = read_symbol_64("locals", &locals);
|
||||||
|
if (error) {
|
||||||
|
perror("locals");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = read_symbol_64("x86_cpu_local_variables_span", &locals_span);
|
||||||
|
if (error) {
|
||||||
|
locals_span = 4096;
|
||||||
|
}
|
||||||
|
if (0) printf("locals 0x%lx span 0x%lx\n", locals, locals_span);
|
||||||
|
|
||||||
|
error = read_symbol_64("clv", &clv);
|
||||||
|
if (error) {
|
||||||
|
perror("clv");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ihk_mc_switch_context = lookup_symbol("ihk_mc_switch_context");
|
||||||
|
if (0) printf("ihk_mc_switch_context: %lx\n", ihk_mc_switch_context);
|
||||||
|
|
||||||
|
for (cpu = 0; cpu < num_processors; ++cpu) {
|
||||||
|
uintptr_t v;
|
||||||
|
uintptr_t head;
|
||||||
|
uintptr_t entry;
|
||||||
|
|
||||||
|
v = clv + (cpu * K(CPU_LOCAL_VAR_SIZE));
|
||||||
|
|
||||||
|
error = read_64(v+K(CURRENT_OFFSET), ¤t);
|
||||||
|
if (error) {
|
||||||
|
perror("current");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
head = v + K(RUNQ_OFFSET);
|
||||||
|
error = read_64(head, &entry);
|
||||||
|
if (error) {
|
||||||
|
perror("runq head");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (entry != head) {
|
||||||
|
uintptr_t thread;
|
||||||
|
uintptr_t proc;
|
||||||
|
int pid;
|
||||||
|
int tid;
|
||||||
|
struct thread_info *ti;
|
||||||
|
int status;
|
||||||
|
|
||||||
|
ti = malloc(sizeof(*ti));
|
||||||
|
if (!ti) {
|
||||||
|
perror("malloc");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
thread = entry - K(SCHED_LIST_OFFSET);
|
||||||
|
|
||||||
|
error = read_64(thread+K(PROC_OFFSET), &proc);
|
||||||
|
if (error) {
|
||||||
|
perror("proc");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = read_32(thread+K(STATUS_OFFSET), &status);
|
||||||
|
if (error) {
|
||||||
|
perror("status");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = read_32(proc+K(PID_OFFSET), &pid);
|
||||||
|
if (error) {
|
||||||
|
perror("pid");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = read_32(thread+K(TID_OFFSET), &tid);
|
||||||
|
if (error) {
|
||||||
|
perror("tid");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ti->next = NULL;
|
||||||
|
ti->status = status;
|
||||||
|
ti->pid = pid;
|
||||||
|
ti->tid = tid;
|
||||||
|
ti->cpu = (thread == current)? cpu: -1;
|
||||||
|
ti->lcpu = cpu;
|
||||||
|
ti->process = thread;
|
||||||
|
ti->clv = v;
|
||||||
|
ti->x86_clv = locals + locals_span*cpu;
|
||||||
|
|
||||||
|
*titailp = ti;
|
||||||
|
titailp = &ti->next;
|
||||||
|
|
||||||
|
error = read_64(entry, &entry);
|
||||||
|
if (error) {
|
||||||
|
perror("process2");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tihead) {
|
||||||
|
printf("thread not found. cpu mode forcibly\n");
|
||||||
|
opt.cpu = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.cpu) {
|
||||||
|
for (cpu = 0; cpu < num_processors; ++cpu) {
|
||||||
|
uintptr_t v;
|
||||||
|
struct thread_info *ti;
|
||||||
|
int status;
|
||||||
|
uintptr_t current;
|
||||||
|
|
||||||
|
v = clv + K(CPU_LOCAL_VAR_SIZE)*cpu;
|
||||||
|
|
||||||
|
error = read_32(v+K(CPU_STATUS_OFFSET), &status);
|
||||||
|
if (error) {
|
||||||
|
perror("cpu.status");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = read_64(v+K(CURRENT_OFFSET), ¤t);
|
||||||
|
if (error) {
|
||||||
|
perror("current");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ti = malloc(sizeof(*ti));
|
||||||
|
if (!ti) {
|
||||||
|
perror("malloc");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ti->next = NULL;
|
||||||
|
ti->status = status << 16;
|
||||||
|
ti->pid = CPU_TID_BASE + cpu;
|
||||||
|
ti->tid = CPU_TID_BASE + cpu;
|
||||||
|
ti->cpu = cpu;
|
||||||
|
ti->process = current;
|
||||||
|
ti->clv = v;
|
||||||
|
ti->x86_clv = locals + locals_span*cpu;
|
||||||
|
|
||||||
|
*titailp = ti;
|
||||||
|
titailp = &ti->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tihead) {
|
||||||
|
printf("thread not found\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
curr_thread = tihead;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} /* setup_threads() */
|
||||||
|
|
||||||
|
static int setup_symbols(char *fname) {
|
||||||
|
ssize_t needs;
|
||||||
|
bfd_boolean ok;
|
||||||
|
|
||||||
|
symbfd = bfd_openr(fname, "elf64-x86-64");
|
||||||
|
if (!symbfd) {
|
||||||
|
bfd_perror("bfd_openr");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = bfd_check_format(symbfd, bfd_object);
|
||||||
|
if (!ok) {
|
||||||
|
bfd_perror("bfd_check_format");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
needs = bfd_get_symtab_upper_bound(symbfd);
|
||||||
|
if (needs < 0) {
|
||||||
|
bfd_perror("bfd_get_symtab_upper_bound");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needs) {
|
||||||
|
printf("no symbols\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
symtab = malloc(needs);
|
||||||
|
if (!symtab) {
|
||||||
|
perror("malloc");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nsyms = bfd_canonicalize_symtab(symbfd, symtab);
|
||||||
|
if (nsyms < 0) {
|
||||||
|
bfd_perror("bfd_canonicalize_symtab");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} /* setup_symbols() */
|
||||||
|
|
||||||
|
static int setup_dump(char *fname) {
|
||||||
|
bfd_boolean ok;
|
||||||
|
|
||||||
|
dumpbfd = bfd_fopen(opt.dump_path, "elf64-x86-64", "r", -1);
|
||||||
|
if (!dumpbfd) {
|
||||||
|
bfd_perror("bfd_fopen");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = bfd_check_format(dumpbfd, bfd_object);
|
||||||
|
if (!ok) {
|
||||||
|
bfd_perror("bfd_check_format");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
dumpscn = bfd_get_section_by_name(dumpbfd, "physmem");
|
||||||
|
if (!dumpscn) {
|
||||||
|
bfd_perror("bfd_get_section_by_name");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
kernel_base = dumpscn->vma + 0x200000;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} /* setup_dump() */
|
||||||
|
|
||||||
|
static ssize_t print_hex(char *buf, char *str) {
|
||||||
|
char *p;
|
||||||
|
char *q;
|
||||||
|
|
||||||
|
q = buf;
|
||||||
|
for (p = str; *p != '\0'; ++p) {
|
||||||
|
q += sprintf(q, "%02x", *p);
|
||||||
|
}
|
||||||
|
*q = '\0';
|
||||||
|
|
||||||
|
return (q - buf);
|
||||||
|
} /* print_hex() */
|
||||||
|
|
||||||
|
static ssize_t print_bin(char *buf, void *data, size_t size) {
|
||||||
|
uint8_t *p;
|
||||||
|
char *q;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
p = data;
|
||||||
|
q = buf;
|
||||||
|
for (i = 0; i < size; ++i) {
|
||||||
|
q += sprintf(q, "%02x", *p);
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
*q = '\0';
|
||||||
|
|
||||||
|
return (q - buf);
|
||||||
|
} /* print_bin() */
|
||||||
|
|
||||||
|
static void command(char *cmd, char *res) {
|
||||||
|
char *p;
|
||||||
|
char *rbp;
|
||||||
|
|
||||||
|
p = cmd;
|
||||||
|
rbp = res;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (!strncmp(p, "qSupported", 10)) {
|
||||||
|
rbp += sprintf(rbp, "PacketSize=1024");
|
||||||
|
rbp += sprintf(rbp, ";qXfer:features:read+");
|
||||||
|
}
|
||||||
|
else if (!strncmp(p, "Hg", 2)) {
|
||||||
|
int n;
|
||||||
|
int tid;
|
||||||
|
struct thread_info *ti;
|
||||||
|
|
||||||
|
p += 2;
|
||||||
|
n = sscanf(p, "%x", &tid);
|
||||||
|
if (n != 1) {
|
||||||
|
printf("cannot parse 'Hg' cmd: \"%s\"\n", p);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (tid) {
|
||||||
|
for (ti = tihead; ti; ti = ti->next) {
|
||||||
|
if (ti->tid == tid) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ti) {
|
||||||
|
printf("invalid tid %#x\n", tid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
curr_thread = ti;
|
||||||
|
}
|
||||||
|
rbp += sprintf(rbp, "OK");
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "Hc-1")) {
|
||||||
|
rbp += sprintf(rbp, "OK");
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "?")) {
|
||||||
|
rbp += sprintf(rbp, "S02");
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "qC")) {
|
||||||
|
rbp += sprintf(rbp, "QC%x", curr_thread->tid);
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "qAttached")) {
|
||||||
|
rbp += sprintf(rbp, "1");
|
||||||
|
}
|
||||||
|
else if (!strncmp(p, "qXfer:features:read:target.xml:", 31)) {
|
||||||
|
char *str =
|
||||||
|
"<target version=\"1.0\">"
|
||||||
|
"<architecture>i386:x86-64</architecture>"
|
||||||
|
"</target>";
|
||||||
|
rbp += sprintf(rbp, "l");
|
||||||
|
if (0)
|
||||||
|
rbp += print_hex(rbp, str);
|
||||||
|
rbp += sprintf(rbp, "%s", str);
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "D")) {
|
||||||
|
rbp += sprintf(rbp, "OK");
|
||||||
|
f_done = 1;
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "g")) {
|
||||||
|
if (curr_thread->cpu < 0) {
|
||||||
|
struct x86_kregs {
|
||||||
|
uintptr_t rsp, rbp, rbx, rsi;
|
||||||
|
uintptr_t rdi, r12, r13, r14;
|
||||||
|
uintptr_t r15, rflags, rsp0;
|
||||||
|
};
|
||||||
|
|
||||||
|
int error;
|
||||||
|
struct x86_kregs kregs;
|
||||||
|
|
||||||
|
error = read_mem(curr_thread->process+K(CTX_OFFSET),
|
||||||
|
&kregs, sizeof(kregs));
|
||||||
|
if (error) {
|
||||||
|
perror("read_mem");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxxxxxxxxxx"); /* rax */
|
||||||
|
rbp += print_bin(rbp, &kregs.rbx, sizeof(uint64_t));
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxxxxxxxxxx"); /* rcx */
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxxxxxxxxxx"); /* rdx */
|
||||||
|
rbp += print_bin(rbp, &kregs.rsi, sizeof(uint64_t));
|
||||||
|
rbp += print_bin(rbp, &kregs.rdi, sizeof(uint64_t));
|
||||||
|
rbp += print_bin(rbp, &kregs.rbp, sizeof(uint64_t));
|
||||||
|
rbp += print_bin(rbp, &kregs.rsp, sizeof(uint64_t));
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxxxxxxxxxx"); /* r8 */
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxxxxxxxxxx"); /* r9 */
|
||||||
|
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxxxxxxxxxx"); /* r10 */
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxxxxxxxxxx"); /* r11 */
|
||||||
|
rbp += print_bin(rbp, &kregs.r12, sizeof(uint64_t));
|
||||||
|
rbp += print_bin(rbp, &kregs.r13, sizeof(uint64_t));
|
||||||
|
rbp += print_bin(rbp, &kregs.r14, sizeof(uint64_t));
|
||||||
|
rbp += print_bin(rbp, &kregs.r15, sizeof(uint64_t));
|
||||||
|
rbp += print_bin(rbp, &ihk_mc_switch_context,
|
||||||
|
sizeof(uint64_t)); /* rip */
|
||||||
|
rbp += print_bin(rbp, &kregs.rflags, sizeof(uint32_t));
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxx"); /* cs */
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxx"); /* ss */
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxx"); /* ds */
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxx"); /* es */
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxx"); /* fs */
|
||||||
|
rbp += sprintf(rbp, "xxxxxxxx"); /* gs */
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int error;
|
||||||
|
uintptr_t regs[21];
|
||||||
|
uint8_t *pu8;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
error = read_mem(curr_thread->x86_clv+240,
|
||||||
|
®s, sizeof(regs));
|
||||||
|
if (error) {
|
||||||
|
perror("read_mem");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
pu8 = (void *)®s;
|
||||||
|
for (i = 0; i < sizeof(regs)-4; ++i) {
|
||||||
|
rbp += sprintf(rbp, "%02x", pu8[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "mffffffff80018a82,1")) {
|
||||||
|
rbp += sprintf(rbp, "b8");
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "mffffffff80018a82,9")) {
|
||||||
|
rbp += sprintf(rbp, "b8f2ffffff41564155");
|
||||||
|
}
|
||||||
|
else if (!strncmp(p, "m", 1)) {
|
||||||
|
int n;
|
||||||
|
uintptr_t start;
|
||||||
|
size_t size;
|
||||||
|
uintptr_t addr;
|
||||||
|
int error;
|
||||||
|
uint8_t u8;
|
||||||
|
|
||||||
|
++p;
|
||||||
|
n = sscanf(p, "%lx,%lx", &start, &size);
|
||||||
|
if (n != 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (addr = start; addr < (start + size); ++addr) {
|
||||||
|
error = read_mem(addr, &u8, sizeof(u8));
|
||||||
|
if (error) {
|
||||||
|
u8 = 0xE5;
|
||||||
|
}
|
||||||
|
rbp += sprintf(rbp, "%02x", u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "qTStatus")) {
|
||||||
|
rbp += sprintf(rbp, "T0;tnotrun:0");
|
||||||
|
}
|
||||||
|
else if (!strncmp(p, "qXfer:memory-map:read::", 23)) {
|
||||||
|
char *str =
|
||||||
|
"<memory-map>"
|
||||||
|
"<memory type=\"rom\" start=\"0xffffffff80001000\" length=\"0x27000\"/>"
|
||||||
|
"</memory-map>";
|
||||||
|
rbp += sprintf(rbp, "l");
|
||||||
|
if (0)
|
||||||
|
rbp += print_hex(rbp, str);
|
||||||
|
rbp += sprintf(rbp, "%s", str);
|
||||||
|
}
|
||||||
|
else if (!strncmp(p, "T", 1)) {
|
||||||
|
int n;
|
||||||
|
int tid;
|
||||||
|
struct thread_info *ti;
|
||||||
|
|
||||||
|
p += 1;
|
||||||
|
n = sscanf(p, "%x", &tid);
|
||||||
|
if (n != 1) {
|
||||||
|
printf("cannot parse 'T' cmd: \"%s\"\n", p);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (ti = tihead; ti; ti = ti->next) {
|
||||||
|
if (ti->tid == tid) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ti) {
|
||||||
|
printf("invalid tid %#x\n", tid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
rbp += sprintf(rbp, "OK");
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "qfThreadInfo")) {
|
||||||
|
struct thread_info *ti;
|
||||||
|
|
||||||
|
for (ti = tihead; ti; ti = ti->next) {
|
||||||
|
if (ti == tihead) {
|
||||||
|
rbp += sprintf(rbp, "m%x", ti->tid);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rbp += sprintf(rbp, ",%x", ti->tid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!strcmp(p, "qsThreadInfo")) {
|
||||||
|
rbp += sprintf(rbp, "l");
|
||||||
|
}
|
||||||
|
else if (!strncmp(p, "qThreadExtraInfo,", 17)) {
|
||||||
|
int n;
|
||||||
|
int tid;
|
||||||
|
struct thread_info *ti;
|
||||||
|
char buf[64];
|
||||||
|
char *q;
|
||||||
|
|
||||||
|
p += 17;
|
||||||
|
n = sscanf(p, "%x", &tid);
|
||||||
|
if (n != 1) {
|
||||||
|
printf("cannot parse 'qThreadExtraInfo' cmd: \"%s\"\n", p);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (ti = tihead; ti; ti = ti->next) {
|
||||||
|
if (ti->tid == tid) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ti) {
|
||||||
|
printf("invalid tid %#x\n", tid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
q = buf;
|
||||||
|
if (ti->status & PS_RUNNING) {
|
||||||
|
q += sprintf(q, "running on cpu%d", ti->cpu);
|
||||||
|
}
|
||||||
|
else if (ti->status & (PS_INTERRUPTIBLE | PS_UNINTERRUPTIBLE)) {
|
||||||
|
q += sprintf(q, "waiting on cpu%d", ti->lcpu);
|
||||||
|
}
|
||||||
|
else if (ti->status & PS_STOPPED) {
|
||||||
|
q += sprintf(q, "stopped on cpu%d", ti->lcpu);
|
||||||
|
}
|
||||||
|
else if (ti->status & PS_TRACED) {
|
||||||
|
q += sprintf(q, "traced on cpu%d", ti->lcpu);
|
||||||
|
}
|
||||||
|
else if (ti->status == CS_IDLE) {
|
||||||
|
q += sprintf(q, "cpu%d idle", ti->cpu);
|
||||||
|
}
|
||||||
|
else if (ti->status == CS_RUNNING) {
|
||||||
|
q += sprintf(q, "cpu%d running", ti->cpu);
|
||||||
|
}
|
||||||
|
else if (ti->status == CS_RESERVED) {
|
||||||
|
q += sprintf(q, "cpu%d reserved", ti->cpu);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
q += sprintf(q, "status=%#x", ti->status);
|
||||||
|
}
|
||||||
|
if (ti->tid != ti->pid) {
|
||||||
|
q += sprintf(q, ",pid=%d", ti->pid);
|
||||||
|
}
|
||||||
|
rbp += print_hex(rbp, buf);
|
||||||
|
}
|
||||||
|
} while (0);
|
||||||
|
|
||||||
|
*rbp = '\0';
|
||||||
|
return;
|
||||||
|
} /* command() */
|
||||||
|
|
||||||
|
static void options(int argc, char *argv[]) {
|
||||||
|
memset(&opt, 0, sizeof(opt));
|
||||||
|
opt.kernel_path = "./kernel.img";
|
||||||
|
opt.dump_path = "./mcdump";
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int c;
|
||||||
|
|
||||||
|
c = getopt(argc, argv, "cd:hk:");
|
||||||
|
if (c < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (c) {
|
||||||
|
case 'h':
|
||||||
|
case '?':
|
||||||
|
opt.help = 1;
|
||||||
|
break;
|
||||||
|
case 'c':
|
||||||
|
opt.cpu = 1;
|
||||||
|
break;
|
||||||
|
case 'k':
|
||||||
|
opt.kernel_path = optarg;
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
opt.dump_path = optarg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (optind < argc) {
|
||||||
|
opt.help = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
} /* options() */
|
||||||
|
|
||||||
|
static int sock = -1;
|
||||||
|
static FILE *ifp = NULL;
|
||||||
|
static FILE *ofp = NULL;
|
||||||
|
|
||||||
|
static int start_gdb(void) {
|
||||||
|
struct sockaddr_in sin;
|
||||||
|
socklen_t slen;
|
||||||
|
int error;
|
||||||
|
pid_t pid;
|
||||||
|
int ss;
|
||||||
|
|
||||||
|
sock = socket(PF_INET, SOCK_STREAM, 0);
|
||||||
|
if (sock < 0) {
|
||||||
|
perror("socket");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = listen(sock, SOMAXCONN);
|
||||||
|
if (error) {
|
||||||
|
perror("listen");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
slen = sizeof(sin);
|
||||||
|
error = getsockname(sock, (struct sockaddr *)&sin, &slen);
|
||||||
|
if (error) {
|
||||||
|
perror("getsockname");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pid = fork();
|
||||||
|
if (pid == (pid_t)-1) {
|
||||||
|
perror("fork");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pid) {
|
||||||
|
char buf[32];
|
||||||
|
|
||||||
|
sprintf(buf, "target remote :%d", ntohs(sin.sin_port));
|
||||||
|
execlp("gdb", "eclair", "-q", "-ex", "set prompt (eclair) ",
|
||||||
|
"-ex", buf, opt.kernel_path, NULL);
|
||||||
|
perror("execlp");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
ss = accept(sock, NULL, NULL);
|
||||||
|
if (ss < 0) {
|
||||||
|
perror("accept");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ifp = fdopen(ss, "r");
|
||||||
|
if (!ifp) {
|
||||||
|
perror("fdopen(r)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ofp = fdopen(ss, "r+");
|
||||||
|
if (!ofp) {
|
||||||
|
perror("fdopen(r+)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} /* start_gdb() */
|
||||||
|
|
||||||
|
static void print_usage(void) {
|
||||||
|
fprintf(stderr, "usage: eclair [-ch] [-d <mcdump>] [-k <kernel.img>]\n");
|
||||||
|
return;
|
||||||
|
} /* print_usage() */
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int c;
|
||||||
|
int error;
|
||||||
|
int mode;
|
||||||
|
uint8_t sum;
|
||||||
|
uint8_t check;
|
||||||
|
static char lbuf[1024];
|
||||||
|
static char rbuf[1024];
|
||||||
|
static char cbuf[3];
|
||||||
|
char *lbp;
|
||||||
|
char *p;
|
||||||
|
|
||||||
|
printf("eclair 0.20151110\n");
|
||||||
|
options(argc, argv);
|
||||||
|
if (opt.help) {
|
||||||
|
print_usage();
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = setup_symbols(opt.kernel_path);
|
||||||
|
if (error) {
|
||||||
|
perror("setup_symbols");
|
||||||
|
print_usage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = setup_dump(opt.dump_path);
|
||||||
|
if (error) {
|
||||||
|
perror("setup_dump");
|
||||||
|
print_usage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = setup_constants();
|
||||||
|
if (error) {
|
||||||
|
perror("setup_constants");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = setup_threads();
|
||||||
|
if (error) {
|
||||||
|
perror("setup_threads");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = start_gdb();
|
||||||
|
if (error) {
|
||||||
|
perror("start_gdb");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mode = 0;
|
||||||
|
sum = 0;
|
||||||
|
lbp = NULL;
|
||||||
|
while (!f_done) {
|
||||||
|
c = fgetc(ifp);
|
||||||
|
if (c < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == 0) {
|
||||||
|
if (c == '$') {
|
||||||
|
mode = 1;
|
||||||
|
sum = 0;
|
||||||
|
lbp = lbuf;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mode == 1) {
|
||||||
|
if (c == '#') {
|
||||||
|
mode = 2;
|
||||||
|
*lbp = '\0';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sum += c;
|
||||||
|
*lbp++ = c;
|
||||||
|
}
|
||||||
|
if (mode == 2) {
|
||||||
|
cbuf[0] = c;
|
||||||
|
mode = 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mode == 3) {
|
||||||
|
cbuf[1] = c;
|
||||||
|
cbuf[2] = '\0';
|
||||||
|
check = strtol(cbuf, NULL, 16);
|
||||||
|
if (check != sum) {
|
||||||
|
mode = 0;
|
||||||
|
fputc('-', ofp);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mode = 0;
|
||||||
|
fputc('+', ofp);
|
||||||
|
command(lbuf, rbuf);
|
||||||
|
sum = 0;
|
||||||
|
for (p = rbuf; *p != '\0'; ++p) {
|
||||||
|
sum += *p;
|
||||||
|
}
|
||||||
|
fprintf(ofp, "$%s#%02x", rbuf, sum);
|
||||||
|
fflush(ofp);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} /* main() */
|
||||||
Reference in New Issue
Block a user