gdbstub initial code
Oleg Nesterov
oleg at redhat.com
Mon Jul 19 16:25:25 UTC 2010
Changes:
- make it work without exporting access_process_vm(). Suprisingly,
kallsyms.c has useful exports.
- Add multiprocess mode. Incomplete, in particular parse_pids()
can't parse the negative hex pids.
At first I tried to support both multiprocess and !multiprocess
modes, but this needs too many "unnecessary" code which I'd like
to avoid, at least now. So this version requires "multiprocess+"
in qSupported, otherwise "target extended-remote" fails.
Please let me know if we need both modes.
Oleg.
-------------- next part --------------
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/utrace.h>
#include <linux/regset.h>
#include <linux/mm.h>
#include <asm/uaccess.h>
static int d_echo;
module_param_named(echo, d_echo, bool, 0);
#define PACKET_SIZE 1024
#define BUFFER_SIZE 1024
struct pbuf {
char *cur, *pkt;
char buf[BUFFER_SIZE];
};
static inline void pb_init(struct pbuf *pb)
{
pb->cur = pb->buf;
pb->pkt = NULL;
}
static inline int pb_size(struct pbuf *pb)
{
return pb->cur - pb->buf;
}
static inline int pb_room(struct pbuf *pb)
{
return pb->buf + BUFFER_SIZE - pb->cur;
}
static inline void pb_putc(struct pbuf *pb, char c)
{
if (WARN_ON(pb->cur >= pb->buf + BUFFER_SIZE-1))
return;
*pb->cur++ = c;
}
static void pb_memcpy(struct pbuf *pb, const void *data, int size)
{
if (WARN_ON(size > pb_room(pb)))
return;
memcpy(pb->cur, data, size);
pb->cur += size;
}
static inline void pb_puts(struct pbuf *pb, const char *s)
{
pb_memcpy(pb, s, strlen(s));
}
static inline void pb_putb(struct pbuf *pb, unsigned char val)
{
static char hex[] = "0123456789abcdef";
pb_putc(pb, hex[(val & 0xf0) >> 4]);
pb_putc(pb, hex[(val & 0x0f) >> 0]);
}
static void pb_putbs(struct pbuf *pb, const char *data, int size)
{
while (size--)
pb_putb(pb, *data++);
}
static inline void pb_start(struct pbuf *pb)
{
WARN_ON(pb->pkt);
pb_putc(pb, '$');
pb->pkt = pb->cur;
}
static inline void pb_cancel(struct pbuf *pb)
{
if (WARN_ON(!pb->pkt))
return;
pb->cur = pb->pkt - 1;
pb->pkt = NULL;
}
static void pb_end(struct pbuf *pb)
{
unsigned char csm = 0;
char *pkt = pb->pkt;
pb->pkt = NULL;
if (WARN_ON(!pkt))
return;
while (pkt < pb->cur) {
WARN_ON(*pkt == '$' || *pkt == '#');
csm += (unsigned char)*pkt++;
}
pb_putc(pb, '#');
pb_putb(pb, csm);
}
static inline void pb_packs(struct pbuf *pb, const char *s)
{
pb_start(pb);
pb_puts(pb, s);
pb_end(pb);
}
static void __attribute__ ((format(printf, 3, 4)))
__pb_format(struct pbuf *pb, bool whole_pkt, const char *fmt, ...)
{
int room = pb_room(pb), size;
va_list args;
if (whole_pkt)
pb_start(pb);
va_start(args, fmt);
size = vsnprintf(pb->cur, room, fmt, args);
va_end(args);
if (WARN_ON(size > room))
return;
pb->cur += size;
if (whole_pkt)
pb_end(pb);
}
#define pb_printf(pb, args...) __pb_format((pb), false, args)
#define pb_packf(pb, args...) __pb_format((pb), true, args)
static inline void *pb_alloc_bs(struct pbuf *pb, int size)
{
if (unlikely(pb_room(pb) < 2 * size + 4))
return NULL;
return pb->cur + size;
}
static inline void *pb_alloc_tmp(struct pbuf *pb, int size)
{
if (unlikely(pb_room(pb) < size))
return NULL;
return pb->cur + BUFFER_SIZE - size;
}
static inline void pb_flush(struct pbuf *pb, int size)
{
int keep = pb_size(pb) - size;
if (keep)
memmove(pb->buf, pb->buf + size, keep);
pb->cur -= size;
}
static int pb_copy_to_user(struct pbuf *pb, char __user *ubuf, int size)
{
int copy = min(size, pb_size(pb));
if (d_echo)
printk(KERN_INFO "<= %.*s\n", copy, pb->buf);
if (copy_to_user(ubuf, pb->buf, copy))
return -EFAULT;
pb_flush(pb, copy);
return copy;
}
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// XXX: TODO: gdb is single-thread, no locking currently.
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#define T printk(KERN_INFO "TRACE: %s:%d\n", __FUNCTION__, __LINE__)
enum {
STOP_RUN,
STOP_REQ,
STOP_ACK,
STOP_FIN,
};
struct ugdb_context {
int c_stop;
int c_exit;
const char *fake_rc;
int c_pid, c_tid;
char c_xid[32];
struct pid *pid;
struct utrace_engine *engine;
struct ugdb *c_ugdb;
struct list_head node;
};
struct ugdb {
char g_ibuf[PACKET_SIZE];
int g_ilen;
struct pbuf g_pbuf;
bool g_no_ack;
int g_err;
struct list_head g_attached;
struct ugdb_context *g_current;
wait_queue_head_t g_wait;
};
// XXX: Of course, this all is racy ----------------------------------
static u32 ugdb_report_quiesce(u32 action, struct utrace_engine *engine,
unsigned long event)
{
struct ugdb_context *context = engine->data;
if (event == UTRACE_EVENT(DEATH)) {
context->c_exit = current->exit_code | INT_MIN;
goto ack;
}
if (context->c_stop == STOP_RUN)
return UTRACE_RESUME;
if (context->c_stop != STOP_REQ)
printk(KERN_INFO "XXX: %d, report_quiesce bad c_stop: %d\n",
context->c_tid, context->c_stop);
ack:
context->c_stop = STOP_ACK;
wake_up_all(&context->c_ugdb->g_wait);
return UTRACE_STOP;
}
static u32 ugdb_report_death(struct utrace_engine *engine,
bool group_dead, int signal)
{
return UTRACE_RESUME;
}
static const struct utrace_engine_ops ugdb_utrace_ops = {
.report_quiesce = ugdb_report_quiesce,
.report_death = ugdb_report_death,
};
static int ugdb_stop_one(struct ugdb_context *context, const char *fake_rc)
{
int err;
if (context->c_stop != STOP_RUN)
return 0;
context->c_stop = STOP_REQ;
context->fake_rc = fake_rc;
err = utrace_control_pid(context->pid, context->engine,
UTRACE_INTERRUPT);
if (err == -EINPROGRESS)
err = 0;
return err;
}
static int ugdb_cont_one(struct ugdb_context *context)
{
if (context->c_stop == STOP_RUN)
return -EALREADY;
context->c_stop = STOP_RUN;
context->fake_rc = NULL;
utrace_control_pid(context->pid, context->engine,
UTRACE_RESUME);
return 0;
}
static int ugdb_wait_for_rc(struct ugdb *ugdb)
{
DEFINE_WAIT(wait);
struct ugdb_context *context;
int ret;
for (;;) {
prepare_to_wait(&ugdb->g_wait, &wait, TASK_INTERRUPTIBLE);
list_for_each_entry(context, &ugdb->g_attached, node) {
if (context->c_stop == STOP_ACK) {
context->c_stop = STOP_FIN;
if (context->c_exit) {
int c = context->c_exit & 0xff;
char r = 'X';
if (!c) {
c = (context->c_exit & 0xff) << 8;
r = 'W';
}
pb_packf(&ugdb->g_pbuf, "%c%02x", r, c);
continue;
}
if (!context->fake_rc) {
printk(KERN_INFO "XXX: %d NULL rc\n",
context->c_tid);
continue;
}
pb_packs(&ugdb->g_pbuf, context->fake_rc);
}
}
ret = 0;
if (pb_size(&ugdb->g_pbuf))
break;
ret = -EINTR;
if (signal_pending(current))
break;
schedule();
}
finish_wait(&ugdb->g_wait, &wait);
return ret;
}
static int xxx_prepare_examine(struct task_struct *task,
struct ugdb_context *context,
struct utrace_examiner *exam)
{
for (;;) {
if (fatal_signal_pending(current))
return -EINTR;
if (context->c_stop == STOP_RUN) {
printk(KERN_INFO "XXX: %d unexpected STOP_RUN\n",
context->c_tid);
return -EINVAL;
}
if (context->c_stop != STOP_REQ) {
int err = utrace_prepare_examine(task, context->engine, exam);
if (!err || err == -ESRCH)
return err;
}
schedule_timeout_interruptible(1);
}
}
static void ugdb_c_all(struct ugdb *ugdb, const char *fake_rc)
{
struct ugdb_context *context;
list_for_each_entry(context, &ugdb->g_attached, node) {
if (fake_rc)
ugdb_stop_one(context, fake_rc);
else
ugdb_cont_one(context);
}
}
static char *handle_vattach(struct ugdb *ugdb, char *cmd)
{
int nr = simple_strtoul(cmd, NULL, 16);
struct pid *pid = find_get_pid(nr);
struct ugdb_context *context;
int err;
if (!pid)
goto err;
context = kzalloc(sizeof(*context), GFP_KERNEL);
if (!context)
goto free_pid;
context->c_pid = nr;
context->c_tid = nr;
snprintf(context->c_xid, sizeof(context->c_xid),
"p%x.%x", context->c_pid, context->c_tid);
context->pid = pid;
context->c_ugdb = ugdb;
context->engine = utrace_attach_pid(pid,
UTRACE_ATTACH_CREATE,
&ugdb_utrace_ops,
context);
if (IS_ERR(context->engine))
goto free_ctx;
err = utrace_set_events_pid(pid, context->engine,
UTRACE_EVENT(QUIESCE) | UTRACE_EVENT(DEATH));
err = ugdb_stop_one(context, "S05");
if (err)
goto free_engine;
list_add_tail(&context->node, &ugdb->g_attached);
ugdb->g_current = context;
return NULL;
free_engine:
utrace_control_pid(pid, context->engine, UTRACE_DETACH);
utrace_engine_put(context->engine);
free_ctx:
kfree(context);
free_pid:
put_pid(pid);
err:
return "E01";
}
static void ugdb_detach_one(struct ugdb *ugdb, struct ugdb_context *context)
{
int ret;
if (ugdb->g_current == context)
ugdb->g_current = NULL;
ret = utrace_control_pid(context->pid, context->engine, UTRACE_DETACH);
if (ret == -EINPROGRESS)
utrace_barrier_pid(context->pid, context->engine);
utrace_engine_put(context->engine);
list_del(&context->node);
put_pid(context->pid);
kfree(context);
}
static bool context_pids(struct ugdb_context *context, int pid, int tid)
{
if (pid > 0 && context->c_pid != pid)
return false;
if (tid > 0 && context->c_tid != tid)
return false;
return true;
}
static void ugdb_detach_many(struct ugdb *ugdb, int pid, int tid)
{
struct ugdb_context *context, *tmp;
list_for_each_entry_safe(context, tmp, &ugdb->g_attached, node)
if (context_pids(context, pid, tid))
ugdb_detach_one(ugdb, context);
}
static struct ugdb_context *find_context(struct ugdb *ugdb, int pid, int tid)
{
struct ugdb_context *context;
list_for_each_entry(context, &ugdb->g_attached, node)
if (context_pids(context, pid, tid))
return context;
return NULL;
}
static bool parse_pids(char *cmd, int *ppid, int *ptid)
{
int r = sscanf(cmd, "p%x.%x", ppid, ptid);
if (r == 2)
return true;
WARN("ERR!! parse_pids(%s) failed\n", cmd);
return false;
}
static char *handle_hg(struct ugdb *ugdb, char *cmd)
{
int pid, tid;
if (parse_pids(cmd, &pid, &tid)) {
ugdb->g_current = find_context(ugdb, pid, tid);
if (ugdb->g_current)
return "OK";
}
return "E01";
}
static const char *handle_t(struct ugdb *ugdb, char *cmd)
{
int pid, tid;
if (!parse_pids(cmd, &pid, &tid))
goto err;
if (pid <= 0 || tid <= 0)
printk(KERN_INFO "XXX: T with multipid? %s\n", cmd);
if (find_context(ugdb, pid, tid))
return "OK";
err:
return "E01";
}
#define REGSET_GENERAL 0
// stolen from gdb-7.1/gdb/gdbserver/linux-x86-low.c
static int x86_64_regmap[] = {
80, 40, 88, 96, 104, 112, 32, 152, 72, 64, 56, 48, 24, 16,
8, 0, 128, 144, 136, 160, 184, 192, 200, 208, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 120,
};
static char *handle_g(struct ugdb *ugdb)
{
struct ugdb_context *context = ugdb->g_current;
const struct user_regset_view *view;
const struct user_regset *rset;
struct utrace_examiner exam;
struct user_regs_struct *pregs;
struct task_struct *task;
int rn;
static int pkt_size;
if (!pkt_size) {
int sz = 0;
for (rn = 0; rn < ARRAY_SIZE(x86_64_regmap); ++rn) {
int offs = x86_64_regmap[rn];
if (offs < 0)
continue;
if (offs > (sizeof(*pregs) - sizeof(long))) {
printk(KERN_INFO "XXX: x86_64_regmap is wrong!\n");
goto err;
}
sz += sizeof(long) * 2;
}
pkt_size = sz;
}
if (!context)
goto err;
if (pb_room(&ugdb->g_pbuf) < 4 + pkt_size + sizeof(*pregs)) {
printk(KERN_INFO "XXX: getregs ENOMEM %d %ld\n",
pkt_size, sizeof(*pregs));
goto err;
}
pregs = pb_alloc_tmp(&ugdb->g_pbuf, sizeof(*pregs));
BUG_ON(pregs + 1 != (void*)ugdb->g_pbuf.cur + BUFFER_SIZE);
task = pid_task(context->pid, PIDTYPE_PID);
if (!task)
goto err;
if (xxx_prepare_examine(task, context, &exam))
goto err;
view = task_user_regset_view(task);
rset = view->regsets + REGSET_GENERAL;
rset->get(task, rset, 0, sizeof(*pregs), pregs, NULL);
if (utrace_finish_examine(task, context->engine, &exam))
goto err;
pb_start(&ugdb->g_pbuf);
for (rn = 0; rn < ARRAY_SIZE(x86_64_regmap); ++rn) {
int offs = x86_64_regmap[rn];
if (offs >= 0)
pb_putbs(&ugdb->g_pbuf, (void*)pregs + offs,
sizeof(long));
}
WARN_ON(pb_room(&ugdb->g_pbuf) < sizeof(*pregs));
pb_end(&ugdb->g_pbuf);
return NULL;
err:
return "E01";
}
static typeof(access_process_vm) *u_access_process_vm;
static void apvm(struct ugdb *ugdb, struct task_struct *task,
unsigned long addr, int size)
{
unsigned char *mbuf;
char *err = "E01";
pb_start(&ugdb->g_pbuf);
mbuf = pb_alloc_bs(&ugdb->g_pbuf, size);
if (!mbuf) {
printk(KERN_INFO "XXX: apvm(%d) ENOMEM\n", size);
goto end;
}
size = u_access_process_vm(task, addr, mbuf, size, 0);
if (size <= 0)
goto end;
pb_putbs(&ugdb->g_pbuf, mbuf, size);
err = "";
end:
pb_puts(&ugdb->g_pbuf, err);
pb_end(&ugdb->g_pbuf);
}
static const char *handle_m(struct ugdb *ugdb, char *cmd)
{
struct ugdb_context *context = ugdb->g_current;
struct utrace_examiner exam;
struct task_struct *task;
unsigned long addr, size;
if (sscanf(cmd, "m%lx,%lx", &addr, &size) != 2)
goto err;
if (!context)
goto err;
task = pid_task(context->pid, PIDTYPE_PID);
if (!task)
goto err;
if (xxx_prepare_examine(task, context, &exam))
goto err;
apvm(ugdb, task, addr, size);
/* Too late to report the error*/
if (utrace_finish_examine(task, context->engine, &exam))
;
return NULL;
err:
return "E01";
}
static const char *handle_d(struct ugdb *ugdb, char *cmd)
{
int nr = simple_strtol(cmd, NULL, 16);
ugdb_detach_many(ugdb, nr, -1);
return "OK";
}
#define EQ(cmd, str) \
(strncmp((cmd), (str), sizeof(str)-1) ? false : \
((cmd) += sizeof(str)-1, true))
static void handle_command(struct ugdb *ugdb, char *cmd, int len)
{
const char *rc = "";
switch (cmd[0]) {
case '!':
rc = "OK";
break;
case '?':
rc = "W00";
break;
case 'g':
rc = handle_g(ugdb);
break;
case 'm':
rc = handle_m(ugdb, cmd);
break;
case 'c':
ugdb_c_all(ugdb, NULL);
rc = NULL;
break;
case 'T':
rc = handle_t(ugdb, cmd + 1);
break;
case 'q':
if (EQ(cmd, "qSupported")) {
if (!strstr(cmd, "multiprocess+")) {
printk(KERN_INFO "ugdb: can't works without multiprocess\n");
ugdb->g_err = -EPROTONOSUPPORT;
break;
}
pb_packf(&ugdb->g_pbuf,
"PacketSize=%x;QStartNoAckMode+;multiprocess+",
PACKET_SIZE);
rc = NULL;
} else if (EQ(cmd, "qC")) {
if (ugdb->g_current) {
pb_packf(&ugdb->g_pbuf, "QC%s",
ugdb->g_current->c_xid);
rc = NULL;
}
} else if (EQ(cmd, "qfThreadInfo")) {
if (ugdb->g_current) {
pb_packf(&ugdb->g_pbuf, "m%s",
ugdb->g_current->c_xid);
rc = NULL;
}
} else if (EQ(cmd, "qsThreadInfo")) {
rc = "l";
} else if (EQ(cmd, "qTStatus")) {
rc = "T0";
}
break;
default:
if (EQ(cmd, "QStartNoAckMode")) {
ugdb->g_no_ack = true;
rc = "OK";
} else if (EQ(cmd, "vAttach;")) {
rc = handle_vattach(ugdb, cmd);
} else if (EQ(cmd, "D;")) {
rc = handle_d(ugdb, cmd);
} else if (EQ(cmd, "Hg")) {
rc = handle_hg(ugdb, cmd);
}
}
if (rc)
pb_packs(&ugdb->g_pbuf, rc);
}
static void process_commands(struct ugdb *ugdb)
{
char *cmds = ugdb->g_ibuf;
int todo = ugdb->g_ilen;
if (d_echo)
printk(KERN_INFO "=> %.*s\n", ugdb->g_ilen, ugdb->g_ibuf);
while (todo) {
char first;
char *c_cmd, *c_end;
int c_len;
first = *cmds++;
todo--;
switch (first) {
default:
// XXX: Ctrl-C sends chr = 3, not implemented.
printk(KERN_INFO "XXX: unknown chr %02x\n", first);
pb_putc(&ugdb->g_pbuf, '-');
break;
case '-':
printk(KERN_INFO "XXX: got NACK!\n");
ugdb->g_err = -EPROTO;
case '+':
break;
case 0x3:
ugdb_c_all(ugdb, "S02");
break;
case '$':
c_cmd = cmds;
c_end = strnchr(c_cmd, todo, '#');
c_len = c_end ? c_end - cmds : -1;
if (c_len < 0 || todo < c_len + 3) {
printk(KERN_INFO "XXX: can't find '#cs'\n");
++todo;
--cmds;
goto out;
}
// XXX: verify checksum ?
todo -= c_len + 3;
cmds += c_len + 3;
*c_end = 0;
if (!ugdb->g_no_ack)
pb_putc(&ugdb->g_pbuf, '+');
handle_command(ugdb, c_cmd, c_len);
}
}
out:
ugdb->g_ilen = todo;
if (todo && cmds > ugdb->g_ibuf)
memcpy(ugdb->g_ibuf, cmds, todo);
}
static struct ugdb *ugdb_inifin(struct ugdb *ugdb)
{
int err = 0;
if (ugdb)
goto dtor;
err = -ENODEV;
// XXX: ugly. proc_reg_open() should take care.
if (!try_module_get(THIS_MODULE))
goto out;
err = -ENOMEM;
ugdb = kzalloc(sizeof(*ugdb), GFP_KERNEL);
if (!ugdb)
goto put_module;
pb_init(&ugdb->g_pbuf);
INIT_LIST_HEAD(&ugdb->g_attached);
init_waitqueue_head(&ugdb->g_wait);
return ugdb;
dtor:
kfree(ugdb);
put_module:
module_put(THIS_MODULE);
out:
return ERR_PTR(err);
}
static int ugdb_f_open(struct inode *inode, struct file *file)
{
nonseekable_open(inode, file);
file->private_data = ugdb_inifin(NULL);
return IS_ERR(file->private_data) ?
PTR_ERR(file->private_data) : 0;
}
static int ugdb_f_release(struct inode *inode, struct file *file)
{
struct ugdb *ugdb = file->private_data;
ugdb_detach_many(ugdb, -1, -1);
ugdb_inifin(ugdb);
return 0;
}
static ssize_t ugdb_f_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
struct ugdb *ugdb = file->private_data;
if (ugdb->g_err)
return ugdb->g_err;
if (count > PACKET_SIZE - ugdb->g_ilen) {
count = PACKET_SIZE - ugdb->g_ilen;
printk("XXX: write(%ld,%d) enospc\n", count, ugdb->g_ilen);
}
if (copy_from_user(ugdb->g_ibuf + ugdb->g_ilen, ubuf, count))
return -EFAULT;
ugdb->g_ilen += count;
process_commands(ugdb);
*ppos += count;
return count;
}
static ssize_t ugdb_f_read(struct file *file, char __user *ubuf,
size_t count, loff_t *ppos)
{
struct ugdb *ugdb = file->private_data;
if (ugdb->g_err)
return ugdb->g_err;
if (!pb_size(&ugdb->g_pbuf)) {
int err = ugdb_wait_for_rc(ugdb);
if (err)
return err;
}
if (pb_size(&ugdb->g_pbuf) > count) {
printk(KERN_INFO "XXX: short read %d %ld\n",
pb_size(&ugdb->g_pbuf), count);
}
count = pb_copy_to_user(&ugdb->g_pbuf, ubuf, count);
if (count > 0)
*ppos += count;
return count;
}
static long ugdb_f_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
// XXX: otherwise gdb->get_tty_state(TCGETS, TCSETS, TCFLSH) complains.
return 0;
}
static const struct file_operations ugdb_f_ops = {
.write = ugdb_f_write,
.read = ugdb_f_read,
.open = ugdb_f_open,
.release = ugdb_f_release,
.unlocked_ioctl = ugdb_f_ioctl,
};
#include <linux/kallsyms.h>
struct kallsyms_sym {
const char *name;
unsigned long addr;
};
static int kallsyms_on_each_symbol_cb(void *data, const char *name,
struct module *mod, unsigned long addr)
{
struct kallsyms_sym *sym = data;
if (strcmp(name, sym->name))
return 0;
sym->addr = addr;
return 1;
}
// XXX: kallsyms_lookup_name() is not exported in 2.6.32
static bool lookup_unexported(void)
{
struct kallsyms_sym sym;
sym.name = "access_process_vm";
if (!kallsyms_on_each_symbol(kallsyms_on_each_symbol_cb, &sym))
goto err;
u_access_process_vm = (void*)sym.addr;
return true;
err:
printk(KERN_ERR "ugdb: can't lookup %s\n", sym.name);
return false;
}
#define PROC_NAME "ugdb"
struct proc_dir_entry *ugdb_pde;
static int __init ugdb_init(void)
{
if (!lookup_unexported())
return -ESRCH;
ugdb_pde = proc_create(PROC_NAME, S_IFREG|S_IRUGO|S_IWUGO,
NULL, &ugdb_f_ops);
if (!ugdb_pde)
return -EBADF;
return 0;
}
static void __exit ugdb_exit(void)
{
remove_proc_entry(PROC_NAME, NULL);
}
MODULE_LICENSE("GPL");
module_init(ugdb_init);
module_exit(ugdb_exit);
More information about the utrace-devel
mailing list