| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * xen console driver interface to hvc_console.c |
| * |
| * (c) 2007 Gerd Hoffmann <kraxel@suse.de> |
| */ |
| |
| #include <linux/console.h> |
| #include <linux/delay.h> |
| #include <linux/err.h> |
| #include <linux/irq.h> |
| #include <linux/init.h> |
| #include <linux/types.h> |
| #include <linux/list.h> |
| #include <linux/serial_core.h> |
| |
| #include <asm/io.h> |
| #include <asm/xen/hypervisor.h> |
| |
| #include <xen/xen.h> |
| #include <xen/interface/xen.h> |
| #include <xen/hvm.h> |
| #include <xen/grant_table.h> |
| #include <xen/page.h> |
| #include <xen/events.h> |
| #include <xen/interface/io/console.h> |
| #include <xen/interface/sched.h> |
| #include <xen/hvc-console.h> |
| #include <xen/xenbus.h> |
| |
| #include "hvc_console.h" |
| |
| #define HVC_COOKIE 0x58656e /* "Xen" in hex */ |
| |
| struct xencons_info { |
| struct list_head list; |
| struct xenbus_device *xbdev; |
| struct xencons_interface *intf; |
| unsigned int evtchn; |
| XENCONS_RING_IDX out_cons; |
| unsigned int out_cons_same; |
| struct hvc_struct *hvc; |
| int irq; |
| int vtermno; |
| grant_ref_t gntref; |
| spinlock_t ring_lock; |
| }; |
| |
| static LIST_HEAD(xenconsoles); |
| static DEFINE_SPINLOCK(xencons_lock); |
| |
| /* ------------------------------------------------------------------ */ |
| |
| static struct xencons_info *vtermno_to_xencons(int vtermno) |
| { |
| struct xencons_info *entry, *ret = NULL; |
| unsigned long flags; |
| |
| spin_lock_irqsave(&xencons_lock, flags); |
| if (list_empty(&xenconsoles)) { |
| spin_unlock_irqrestore(&xencons_lock, flags); |
| return NULL; |
| } |
| |
| list_for_each_entry(entry, &xenconsoles, list) { |
| if (entry->vtermno == vtermno) { |
| ret = entry; |
| break; |
| } |
| } |
| spin_unlock_irqrestore(&xencons_lock, flags); |
| |
| return ret; |
| } |
| |
| static inline int xenbus_devid_to_vtermno(int devid) |
| { |
| return devid + HVC_COOKIE; |
| } |
| |
| static inline void notify_daemon(struct xencons_info *cons) |
| { |
| /* Use evtchn: this is called early, before irq is set up. */ |
| notify_remote_via_evtchn(cons->evtchn); |
| } |
| |
| static ssize_t __write_console(struct xencons_info *xencons, |
| const u8 *data, size_t len) |
| { |
| XENCONS_RING_IDX cons, prod; |
| struct xencons_interface *intf = xencons->intf; |
| unsigned long flags; |
| size_t sent = 0; |
| |
| spin_lock_irqsave(&xencons->ring_lock, flags); |
| cons = intf->out_cons; |
| prod = intf->out_prod; |
| mb(); /* update queue values before going on */ |
| |
| if ((prod - cons) > sizeof(intf->out)) { |
| spin_unlock_irqrestore(&xencons->ring_lock, flags); |
| pr_err_once("xencons: Illegal ring page indices"); |
| return -EINVAL; |
| } |
| |
| while ((sent < len) && ((prod - cons) < sizeof(intf->out))) |
| intf->out[MASK_XENCONS_IDX(prod++, intf->out)] = data[sent++]; |
| |
| wmb(); /* write ring before updating pointer */ |
| intf->out_prod = prod; |
| spin_unlock_irqrestore(&xencons->ring_lock, flags); |
| |
| if (sent) |
| notify_daemon(xencons); |
| return sent; |
| } |
| |
| static ssize_t domU_write_console(uint32_t vtermno, const u8 *data, size_t len) |
| { |
| struct xencons_info *cons = vtermno_to_xencons(vtermno); |
| size_t ret = len; |
| |
| if (cons == NULL) |
| return -EINVAL; |
| |
| /* |
| * Make sure the whole buffer is emitted, polling if |
| * necessary. We don't ever want to rely on the hvc daemon |
| * because the most interesting console output is when the |
| * kernel is crippled. |
| */ |
| while (len) { |
| ssize_t sent = __write_console(cons, data, len); |
| |
| if (sent < 0) |
| return sent; |
| |
| data += sent; |
| len -= sent; |
| |
| if (unlikely(len)) |
| HYPERVISOR_sched_op(SCHEDOP_yield, NULL); |
| } |
| |
| return ret; |
| } |
| |
| static ssize_t domU_read_console(uint32_t vtermno, u8 *buf, size_t len) |
| { |
| struct xencons_interface *intf; |
| XENCONS_RING_IDX cons, prod; |
| struct xencons_info *xencons = vtermno_to_xencons(vtermno); |
| unsigned int eoiflag = 0; |
| unsigned long flags; |
| size_t recv = 0; |
| |
| if (xencons == NULL) |
| return -EINVAL; |
| intf = xencons->intf; |
| |
| spin_lock_irqsave(&xencons->ring_lock, flags); |
| cons = intf->in_cons; |
| prod = intf->in_prod; |
| mb(); /* get pointers before reading ring */ |
| |
| if ((prod - cons) > sizeof(intf->in)) { |
| spin_unlock_irqrestore(&xencons->ring_lock, flags); |
| pr_err_once("xencons: Illegal ring page indices"); |
| return -EINVAL; |
| } |
| |
| while (cons != prod && recv < len) |
| buf[recv++] = intf->in[MASK_XENCONS_IDX(cons++, intf->in)]; |
| |
| mb(); /* read ring before consuming */ |
| intf->in_cons = cons; |
| |
| /* |
| * When to mark interrupt having been spurious: |
| * - there was no new data to be read, and |
| * - the backend did not consume some output bytes, and |
| * - the previous round with no read data didn't see consumed bytes |
| * (we might have a race with an interrupt being in flight while |
| * updating xencons->out_cons, so account for that by allowing one |
| * round without any visible reason) |
| */ |
| if (intf->out_cons != xencons->out_cons) { |
| xencons->out_cons = intf->out_cons; |
| xencons->out_cons_same = 0; |
| } |
| if (!recv && xencons->out_cons_same++ > 1) { |
| eoiflag = XEN_EOI_FLAG_SPURIOUS; |
| } |
| spin_unlock_irqrestore(&xencons->ring_lock, flags); |
| |
| if (recv) { |
| notify_daemon(xencons); |
| } |
| |
| xen_irq_lateeoi(xencons->irq, eoiflag); |
| |
| return recv; |
| } |
| |
| static const struct hv_ops domU_hvc_ops = { |
| .get_chars = domU_read_console, |
| .put_chars = domU_write_console, |
| .notifier_add = notifier_add_irq, |
| .notifier_del = notifier_del_irq, |
| .notifier_hangup = notifier_hangup_irq, |
| }; |
| |
| static ssize_t dom0_read_console(uint32_t vtermno, u8 *buf, size_t len) |
| { |
| return HYPERVISOR_console_io(CONSOLEIO_read, len, buf); |
| } |
| |
| /* |
| * Either for a dom0 to write to the system console, or a domU with a |
| * debug version of Xen |
| */ |
| static ssize_t dom0_write_console(uint32_t vtermno, const u8 *str, size_t len) |
| { |
| int rc = HYPERVISOR_console_io(CONSOLEIO_write, len, (u8 *)str); |
| if (rc < 0) |
| return rc; |
| |
| return len; |
| } |
| |
| static const struct hv_ops dom0_hvc_ops = { |
| .get_chars = dom0_read_console, |
| .put_chars = dom0_write_console, |
| .notifier_add = notifier_add_irq, |
| .notifier_del = notifier_del_irq, |
| .notifier_hangup = notifier_hangup_irq, |
| }; |
| |
| static int xen_hvm_console_init(void) |
| { |
| int r; |
| uint64_t v = 0; |
| unsigned long gfn, flags; |
| struct xencons_info *info; |
| |
| if (!xen_hvm_domain()) |
| return -ENODEV; |
| |
| info = vtermno_to_xencons(HVC_COOKIE); |
| if (!info) { |
| info = kzalloc(sizeof(struct xencons_info), GFP_KERNEL); |
| if (!info) |
| return -ENOMEM; |
| spin_lock_init(&info->ring_lock); |
| } else if (info->intf != NULL) { |
| /* already configured */ |
| return 0; |
| } |
| /* |
| * If the toolstack (or the hypervisor) hasn't set these values, the |
| * default value is 0. Even though gfn = 0 and evtchn = 0 are |
| * theoretically correct values, in practice they never are and they |
| * mean that a legacy toolstack hasn't initialized the pv console correctly. |
| */ |
| r = hvm_get_parameter(HVM_PARAM_CONSOLE_EVTCHN, &v); |
| if (r < 0 || v == 0) |
| goto err; |
| info->evtchn = v; |
| v = 0; |
| r = hvm_get_parameter(HVM_PARAM_CONSOLE_PFN, &v); |
| if (r < 0 || v == 0) |
| goto err; |
| gfn = v; |
| info->intf = memremap(gfn << XEN_PAGE_SHIFT, XEN_PAGE_SIZE, MEMREMAP_WB); |
| if (info->intf == NULL) |
| goto err; |
| info->vtermno = HVC_COOKIE; |
| |
| spin_lock_irqsave(&xencons_lock, flags); |
| list_add_tail(&info->list, &xenconsoles); |
| spin_unlock_irqrestore(&xencons_lock, flags); |
| |
| return 0; |
| err: |
| kfree(info); |
| return -ENODEV; |
| } |
| |
| static int xencons_info_pv_init(struct xencons_info *info, int vtermno) |
| { |
| spin_lock_init(&info->ring_lock); |
| info->evtchn = xen_start_info->console.domU.evtchn; |
| /* GFN == MFN for PV guest */ |
| info->intf = gfn_to_virt(xen_start_info->console.domU.mfn); |
| info->vtermno = vtermno; |
| |
| list_add_tail(&info->list, &xenconsoles); |
| |
| return 0; |
| } |
| |
| static int xen_pv_console_init(void) |
| { |
| struct xencons_info *info; |
| unsigned long flags; |
| |
| if (!xen_pv_domain()) |
| return -ENODEV; |
| |
| if (!xen_start_info->console.domU.evtchn) |
| return -ENODEV; |
| |
| info = vtermno_to_xencons(HVC_COOKIE); |
| if (!info) { |
| info = kzalloc(sizeof(struct xencons_info), GFP_KERNEL); |
| if (!info) |
| return -ENOMEM; |
| } else if (info->intf != NULL) { |
| /* already configured */ |
| return 0; |
| } |
| spin_lock_irqsave(&xencons_lock, flags); |
| xencons_info_pv_init(info, HVC_COOKIE); |
| spin_unlock_irqrestore(&xencons_lock, flags); |
| |
| return 0; |
| } |
| |
| static int xen_initial_domain_console_init(void) |
| { |
| struct xencons_info *info; |
| unsigned long flags; |
| |
| if (!xen_initial_domain()) |
| return -ENODEV; |
| |
| info = vtermno_to_xencons(HVC_COOKIE); |
| if (!info) { |
| info = kzalloc(sizeof(struct xencons_info), GFP_KERNEL); |
| if (!info) |
| return -ENOMEM; |
| spin_lock_init(&info->ring_lock); |
| } |
| |
| info->irq = bind_virq_to_irq(VIRQ_CONSOLE, 0, false); |
| info->vtermno = HVC_COOKIE; |
| |
| spin_lock_irqsave(&xencons_lock, flags); |
| list_add_tail(&info->list, &xenconsoles); |
| spin_unlock_irqrestore(&xencons_lock, flags); |
| |
| return 0; |
| } |
| |
| static void xen_console_update_evtchn(struct xencons_info *info) |
| { |
| if (xen_hvm_domain()) { |
| uint64_t v = 0; |
| int err; |
| |
| err = hvm_get_parameter(HVM_PARAM_CONSOLE_EVTCHN, &v); |
| if (!err && v) |
| info->evtchn = v; |
| } else |
| info->evtchn = xen_start_info->console.domU.evtchn; |
| } |
| |
| void xen_console_resume(void) |
| { |
| struct xencons_info *info = vtermno_to_xencons(HVC_COOKIE); |
| if (info != NULL && info->irq) { |
| if (!xen_initial_domain()) |
| xen_console_update_evtchn(info); |
| rebind_evtchn_irq(info->evtchn, info->irq); |
| } |
| } |
| |
| #ifdef CONFIG_HVC_XEN_FRONTEND |
| static void xencons_disconnect_backend(struct xencons_info *info) |
| { |
| if (info->hvc != NULL) |
| hvc_remove(info->hvc); |
| info->hvc = NULL; |
| if (info->irq > 0) { |
| evtchn_put(info->evtchn); |
| info->irq = 0; |
| info->evtchn = 0; |
| } |
| /* evtchn_put() will also close it so this is only an error path */ |
| if (info->evtchn > 0) |
| xenbus_free_evtchn(info->xbdev, info->evtchn); |
| info->evtchn = 0; |
| if (info->gntref > 0) |
| gnttab_free_grant_references(info->gntref); |
| info->gntref = 0; |
| } |
| |
| static void xencons_free(struct xencons_info *info) |
| { |
| free_page((unsigned long)info->intf); |
| info->intf = NULL; |
| info->vtermno = 0; |
| kfree(info); |
| } |
| |
| static int xen_console_remove(struct xencons_info *info) |
| { |
| unsigned long flags; |
| |
| xencons_disconnect_backend(info); |
| spin_lock_irqsave(&xencons_lock, flags); |
| list_del(&info->list); |
| spin_unlock_irqrestore(&xencons_lock, flags); |
| if (info->xbdev != NULL) |
| xencons_free(info); |
| else { |
| if (xen_hvm_domain()) |
| iounmap(info->intf); |
| kfree(info); |
| } |
| return 0; |
| } |
| |
| static void xencons_remove(struct xenbus_device *dev) |
| { |
| xen_console_remove(dev_get_drvdata(&dev->dev)); |
| } |
| |
| static int xencons_connect_backend(struct xenbus_device *dev, |
| struct xencons_info *info) |
| { |
| int ret, evtchn, devid, ref, irq; |
| struct xenbus_transaction xbt; |
| grant_ref_t gref_head; |
| |
| ret = xenbus_alloc_evtchn(dev, &evtchn); |
| if (ret) |
| return ret; |
| info->evtchn = evtchn; |
| irq = bind_evtchn_to_irq_lateeoi(evtchn); |
| if (irq < 0) |
| return irq; |
| info->irq = irq; |
| devid = dev->nodename[strlen(dev->nodename) - 1] - '0'; |
| info->hvc = hvc_alloc(xenbus_devid_to_vtermno(devid), |
| irq, &domU_hvc_ops, 256); |
| if (IS_ERR(info->hvc)) |
| return PTR_ERR(info->hvc); |
| ret = gnttab_alloc_grant_references(1, &gref_head); |
| if (ret < 0) |
| return ret; |
| info->gntref = gref_head; |
| ref = gnttab_claim_grant_reference(&gref_head); |
| if (ref < 0) |
| return ref; |
| gnttab_grant_foreign_access_ref(ref, info->xbdev->otherend_id, |
| virt_to_gfn(info->intf), 0); |
| |
| again: |
| ret = xenbus_transaction_start(&xbt); |
| if (ret) { |
| xenbus_dev_fatal(dev, ret, "starting transaction"); |
| return ret; |
| } |
| ret = xenbus_printf(xbt, dev->nodename, "ring-ref", "%d", ref); |
| if (ret) |
| goto error_xenbus; |
| ret = xenbus_printf(xbt, dev->nodename, "port", "%u", |
| evtchn); |
| if (ret) |
| goto error_xenbus; |
| ret = xenbus_transaction_end(xbt, 0); |
| if (ret) { |
| if (ret == -EAGAIN) |
| goto again; |
| xenbus_dev_fatal(dev, ret, "completing transaction"); |
| return ret; |
| } |
| |
| xenbus_switch_state(dev, XenbusStateInitialised); |
| return 0; |
| |
| error_xenbus: |
| xenbus_transaction_end(xbt, 1); |
| xenbus_dev_fatal(dev, ret, "writing xenstore"); |
| return ret; |
| } |
| |
| static int xencons_probe(struct xenbus_device *dev, |
| const struct xenbus_device_id *id) |
| { |
| int ret, devid; |
| struct xencons_info *info; |
| unsigned long flags; |
| |
| devid = dev->nodename[strlen(dev->nodename) - 1] - '0'; |
| if (devid == 0) |
| return -ENODEV; |
| |
| info = kzalloc(sizeof(struct xencons_info), GFP_KERNEL); |
| if (!info) |
| return -ENOMEM; |
| spin_lock_init(&info->ring_lock); |
| dev_set_drvdata(&dev->dev, info); |
| info->xbdev = dev; |
| info->vtermno = xenbus_devid_to_vtermno(devid); |
| info->intf = (void *)__get_free_page(GFP_KERNEL | __GFP_ZERO); |
| if (!info->intf) |
| goto error_nomem; |
| |
| ret = xencons_connect_backend(dev, info); |
| if (ret < 0) |
| goto error; |
| spin_lock_irqsave(&xencons_lock, flags); |
| list_add_tail(&info->list, &xenconsoles); |
| spin_unlock_irqrestore(&xencons_lock, flags); |
| |
| return 0; |
| |
| error_nomem: |
| ret = -ENOMEM; |
| xenbus_dev_fatal(dev, ret, "allocating device memory"); |
| error: |
| xencons_disconnect_backend(info); |
| xencons_free(info); |
| return ret; |
| } |
| |
| static int xencons_resume(struct xenbus_device *dev) |
| { |
| struct xencons_info *info = dev_get_drvdata(&dev->dev); |
| |
| xencons_disconnect_backend(info); |
| memset(info->intf, 0, XEN_PAGE_SIZE); |
| return xencons_connect_backend(dev, info); |
| } |
| |
| static void xencons_backend_changed(struct xenbus_device *dev, |
| enum xenbus_state backend_state) |
| { |
| switch (backend_state) { |
| case XenbusStateReconfiguring: |
| case XenbusStateReconfigured: |
| case XenbusStateInitialising: |
| case XenbusStateInitialised: |
| case XenbusStateUnknown: |
| break; |
| |
| case XenbusStateInitWait: |
| break; |
| |
| case XenbusStateConnected: |
| xenbus_switch_state(dev, XenbusStateConnected); |
| break; |
| |
| case XenbusStateClosed: |
| if (dev->state == XenbusStateClosed) |
| break; |
| fallthrough; /* Missed the backend's CLOSING state */ |
| case XenbusStateClosing: { |
| struct xencons_info *info = dev_get_drvdata(&dev->dev); |
| |
| /* |
| * Don't tear down the evtchn and grant ref before the other |
| * end has disconnected, but do stop userspace from trying |
| * to use the device before we allow the backend to close. |
| */ |
| if (info->hvc) { |
| hvc_remove(info->hvc); |
| info->hvc = NULL; |
| } |
| |
| xenbus_frontend_closed(dev); |
| break; |
| } |
| } |
| } |
| |
| static const struct xenbus_device_id xencons_ids[] = { |
| { "console" }, |
| { "" } |
| }; |
| |
| static struct xenbus_driver xencons_driver = { |
| .name = "xenconsole", |
| .ids = xencons_ids, |
| .probe = xencons_probe, |
| .remove = xencons_remove, |
| .resume = xencons_resume, |
| .otherend_changed = xencons_backend_changed, |
| .not_essential = true, |
| }; |
| #endif /* CONFIG_HVC_XEN_FRONTEND */ |
| |
| static int __init xen_hvc_init(void) |
| { |
| int r; |
| struct xencons_info *info; |
| const struct hv_ops *ops; |
| |
| if (!xen_domain()) |
| return -ENODEV; |
| |
| if (xen_initial_domain()) { |
| ops = &dom0_hvc_ops; |
| r = xen_initial_domain_console_init(); |
| if (r < 0) |
| goto register_fe; |
| info = vtermno_to_xencons(HVC_COOKIE); |
| } else { |
| ops = &domU_hvc_ops; |
| if (xen_hvm_domain()) |
| r = xen_hvm_console_init(); |
| else |
| r = xen_pv_console_init(); |
| if (r < 0) |
| goto register_fe; |
| |
| info = vtermno_to_xencons(HVC_COOKIE); |
| info->irq = bind_evtchn_to_irq_lateeoi(info->evtchn); |
| } |
| if (info->irq < 0) |
| info->irq = 0; /* NO_IRQ */ |
| else |
| irq_set_noprobe(info->irq); |
| |
| info->hvc = hvc_alloc(HVC_COOKIE, info->irq, ops, 256); |
| if (IS_ERR(info->hvc)) { |
| unsigned long flags; |
| |
| r = PTR_ERR(info->hvc); |
| spin_lock_irqsave(&xencons_lock, flags); |
| list_del(&info->list); |
| spin_unlock_irqrestore(&xencons_lock, flags); |
| if (info->irq) |
| evtchn_put(info->evtchn); |
| kfree(info); |
| return r; |
| } |
| |
| r = 0; |
| register_fe: |
| #ifdef CONFIG_HVC_XEN_FRONTEND |
| r = xenbus_register_frontend(&xencons_driver); |
| #endif |
| return r; |
| } |
| device_initcall(xen_hvc_init); |
| |
| static int xen_cons_init(void) |
| { |
| const struct hv_ops *ops; |
| |
| if (!xen_domain()) |
| return 0; |
| |
| if (xen_initial_domain()) |
| ops = &dom0_hvc_ops; |
| else { |
| int r; |
| ops = &domU_hvc_ops; |
| |
| if (xen_hvm_domain()) |
| r = xen_hvm_console_init(); |
| else |
| r = xen_pv_console_init(); |
| if (r < 0) |
| return r; |
| } |
| |
| hvc_instantiate(HVC_COOKIE, 0, ops); |
| return 0; |
| } |
| console_initcall(xen_cons_init); |
| |
| #ifdef CONFIG_X86 |
| static void xen_hvm_early_write(uint32_t vtermno, const char *str, int len) |
| { |
| if (xen_cpuid_base()) |
| outsb(0xe9, str, len); |
| } |
| #else |
| static void xen_hvm_early_write(uint32_t vtermno, const char *str, int len) { } |
| #endif |
| |
| #ifdef CONFIG_EARLY_PRINTK |
| static int __init xenboot_console_setup(struct console *console, char *string) |
| { |
| static struct xencons_info xenboot; |
| |
| if (xen_initial_domain() || !xen_pv_domain()) |
| return 0; |
| |
| return xencons_info_pv_init(&xenboot, 0); |
| } |
| |
| static void xenboot_write_console(struct console *console, const char *string, |
| unsigned len) |
| { |
| unsigned int linelen, off = 0; |
| const char *pos; |
| |
| if (dom0_write_console(0, string, len) >= 0) |
| return; |
| |
| if (!xen_pv_domain()) { |
| xen_hvm_early_write(0, string, len); |
| return; |
| } |
| |
| if (domU_write_console(0, "(early) ", 8) < 0) |
| return; |
| while (off < len && NULL != (pos = strchr(string+off, '\n'))) { |
| linelen = pos-string+off; |
| if (off + linelen > len) |
| break; |
| domU_write_console(0, string+off, linelen); |
| domU_write_console(0, "\r\n", 2); |
| off += linelen + 1; |
| } |
| if (off < len) |
| domU_write_console(0, string+off, len-off); |
| } |
| |
| struct console xenboot_console = { |
| .name = "xenboot", |
| .write = xenboot_write_console, |
| .setup = xenboot_console_setup, |
| .flags = CON_PRINTBUFFER | CON_BOOT | CON_ANYTIME, |
| .index = -1, |
| }; |
| #endif /* CONFIG_EARLY_PRINTK */ |
| |
| void xen_raw_console_write(const char *str) |
| { |
| ssize_t len = strlen(str); |
| int rc = 0; |
| |
| if (xen_domain()) { |
| rc = dom0_write_console(0, str, len); |
| if (rc != -ENOSYS || !xen_hvm_domain()) |
| return; |
| } |
| xen_hvm_early_write(0, str, len); |
| } |
| |
| void xen_raw_printk(const char *fmt, ...) |
| { |
| static char buf[512]; |
| va_list ap; |
| |
| va_start(ap, fmt); |
| vsnprintf(buf, sizeof(buf), fmt, ap); |
| va_end(ap); |
| |
| xen_raw_console_write(buf); |
| } |
| |
| static void xenboot_earlycon_write(struct console *console, |
| const char *string, |
| unsigned len) |
| { |
| dom0_write_console(0, string, len); |
| } |
| |
| static int __init xenboot_earlycon_setup(struct earlycon_device *device, |
| const char *opt) |
| { |
| device->con->write = xenboot_earlycon_write; |
| return 0; |
| } |
| EARLYCON_DECLARE(xenboot, xenboot_earlycon_setup); |