|  | /* | 
|  | *  This code exports profiling data as debugfs files to userspace. | 
|  | * | 
|  | *    Copyright IBM Corp. 2009 | 
|  | *    Author(s): Peter Oberparleiter <oberpar@linux.vnet.ibm.com> | 
|  | * | 
|  | *    Uses gcc-internal data definitions. | 
|  | *    Based on the gcov-kernel patch by: | 
|  | *		 Hubertus Franke <frankeh@us.ibm.com> | 
|  | *		 Nigel Hinds <nhinds@us.ibm.com> | 
|  | *		 Rajan Ravindran <rajancr@us.ibm.com> | 
|  | *		 Peter Oberparleiter <oberpar@linux.vnet.ibm.com> | 
|  | *		 Paul Larson | 
|  | *		 Yi CDL Yang | 
|  | */ | 
|  |  | 
|  | #define pr_fmt(fmt)	"gcov: " fmt | 
|  |  | 
|  | #include <linux/init.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/debugfs.h> | 
|  | #include <linux/fs.h> | 
|  | #include <linux/list.h> | 
|  | #include <linux/string.h> | 
|  | #include <linux/slab.h> | 
|  | #include <linux/mutex.h> | 
|  | #include <linux/seq_file.h> | 
|  | #include "gcov.h" | 
|  |  | 
|  | /** | 
|  | * struct gcov_node - represents a debugfs entry | 
|  | * @list: list head for child node list | 
|  | * @children: child nodes | 
|  | * @all: list head for list of all nodes | 
|  | * @parent: parent node | 
|  | * @info: associated profiling data structure if not a directory | 
|  | * @ghost: when an object file containing profiling data is unloaded we keep a | 
|  | *         copy of the profiling data here to allow collecting coverage data | 
|  | *         for cleanup code. Such a node is called a "ghost". | 
|  | * @dentry: main debugfs entry, either a directory or data file | 
|  | * @links: associated symbolic links | 
|  | * @name: data file basename | 
|  | * | 
|  | * struct gcov_node represents an entity within the gcov/ subdirectory | 
|  | * of debugfs. There are directory and data file nodes. The latter represent | 
|  | * the actual synthesized data file plus any associated symbolic links which | 
|  | * are needed by the gcov tool to work correctly. | 
|  | */ | 
|  | struct gcov_node { | 
|  | struct list_head list; | 
|  | struct list_head children; | 
|  | struct list_head all; | 
|  | struct gcov_node *parent; | 
|  | struct gcov_info *info; | 
|  | struct gcov_info *ghost; | 
|  | struct dentry *dentry; | 
|  | struct dentry **links; | 
|  | char name[0]; | 
|  | }; | 
|  |  | 
|  | static const char objtree[] = OBJTREE; | 
|  | static const char srctree[] = SRCTREE; | 
|  | static struct gcov_node root_node; | 
|  | static struct dentry *reset_dentry; | 
|  | static LIST_HEAD(all_head); | 
|  | static DEFINE_MUTEX(node_lock); | 
|  |  | 
|  | /* If non-zero, keep copies of profiling data for unloaded modules. */ | 
|  | static int gcov_persist = 1; | 
|  |  | 
|  | static int __init gcov_persist_setup(char *str) | 
|  | { | 
|  | unsigned long val; | 
|  |  | 
|  | if (strict_strtoul(str, 0, &val)) { | 
|  | pr_warning("invalid gcov_persist parameter '%s'\n", str); | 
|  | return 0; | 
|  | } | 
|  | gcov_persist = val; | 
|  | pr_info("setting gcov_persist to %d\n", gcov_persist); | 
|  |  | 
|  | return 1; | 
|  | } | 
|  | __setup("gcov_persist=", gcov_persist_setup); | 
|  |  | 
|  | /* | 
|  | * seq_file.start() implementation for gcov data files. Note that the | 
|  | * gcov_iterator interface is designed to be more restrictive than seq_file | 
|  | * (no start from arbitrary position, etc.), to simplify the iterator | 
|  | * implementation. | 
|  | */ | 
|  | static void *gcov_seq_start(struct seq_file *seq, loff_t *pos) | 
|  | { | 
|  | loff_t i; | 
|  |  | 
|  | gcov_iter_start(seq->private); | 
|  | for (i = 0; i < *pos; i++) { | 
|  | if (gcov_iter_next(seq->private)) | 
|  | return NULL; | 
|  | } | 
|  | return seq->private; | 
|  | } | 
|  |  | 
|  | /* seq_file.next() implementation for gcov data files. */ | 
|  | static void *gcov_seq_next(struct seq_file *seq, void *data, loff_t *pos) | 
|  | { | 
|  | struct gcov_iterator *iter = data; | 
|  |  | 
|  | if (gcov_iter_next(iter)) | 
|  | return NULL; | 
|  | (*pos)++; | 
|  |  | 
|  | return iter; | 
|  | } | 
|  |  | 
|  | /* seq_file.show() implementation for gcov data files. */ | 
|  | static int gcov_seq_show(struct seq_file *seq, void *data) | 
|  | { | 
|  | struct gcov_iterator *iter = data; | 
|  |  | 
|  | if (gcov_iter_write(iter, seq)) | 
|  | return -EINVAL; | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void gcov_seq_stop(struct seq_file *seq, void *data) | 
|  | { | 
|  | /* Unused. */ | 
|  | } | 
|  |  | 
|  | static const struct seq_operations gcov_seq_ops = { | 
|  | .start	= gcov_seq_start, | 
|  | .next	= gcov_seq_next, | 
|  | .show	= gcov_seq_show, | 
|  | .stop	= gcov_seq_stop, | 
|  | }; | 
|  |  | 
|  | /* | 
|  | * Return the profiling data set for a given node. This can either be the | 
|  | * original profiling data structure or a duplicate (also called "ghost") | 
|  | * in case the associated object file has been unloaded. | 
|  | */ | 
|  | static struct gcov_info *get_node_info(struct gcov_node *node) | 
|  | { | 
|  | if (node->info) | 
|  | return node->info; | 
|  |  | 
|  | return node->ghost; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * open() implementation for gcov data files. Create a copy of the profiling | 
|  | * data set and initialize the iterator and seq_file interface. | 
|  | */ | 
|  | static int gcov_seq_open(struct inode *inode, struct file *file) | 
|  | { | 
|  | struct gcov_node *node = inode->i_private; | 
|  | struct gcov_iterator *iter; | 
|  | struct seq_file *seq; | 
|  | struct gcov_info *info; | 
|  | int rc = -ENOMEM; | 
|  |  | 
|  | mutex_lock(&node_lock); | 
|  | /* | 
|  | * Read from a profiling data copy to minimize reference tracking | 
|  | * complexity and concurrent access. | 
|  | */ | 
|  | info = gcov_info_dup(get_node_info(node)); | 
|  | if (!info) | 
|  | goto out_unlock; | 
|  | iter = gcov_iter_new(info); | 
|  | if (!iter) | 
|  | goto err_free_info; | 
|  | rc = seq_open(file, &gcov_seq_ops); | 
|  | if (rc) | 
|  | goto err_free_iter_info; | 
|  | seq = file->private_data; | 
|  | seq->private = iter; | 
|  | out_unlock: | 
|  | mutex_unlock(&node_lock); | 
|  | return rc; | 
|  |  | 
|  | err_free_iter_info: | 
|  | gcov_iter_free(iter); | 
|  | err_free_info: | 
|  | gcov_info_free(info); | 
|  | goto out_unlock; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * release() implementation for gcov data files. Release resources allocated | 
|  | * by open(). | 
|  | */ | 
|  | static int gcov_seq_release(struct inode *inode, struct file *file) | 
|  | { | 
|  | struct gcov_iterator *iter; | 
|  | struct gcov_info *info; | 
|  | struct seq_file *seq; | 
|  |  | 
|  | seq = file->private_data; | 
|  | iter = seq->private; | 
|  | info = gcov_iter_get_info(iter); | 
|  | gcov_iter_free(iter); | 
|  | gcov_info_free(info); | 
|  | seq_release(inode, file); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Find a node by the associated data file name. Needs to be called with | 
|  | * node_lock held. | 
|  | */ | 
|  | static struct gcov_node *get_node_by_name(const char *name) | 
|  | { | 
|  | struct gcov_node *node; | 
|  | struct gcov_info *info; | 
|  |  | 
|  | list_for_each_entry(node, &all_head, all) { | 
|  | info = get_node_info(node); | 
|  | if (info && (strcmp(info->filename, name) == 0)) | 
|  | return node; | 
|  | } | 
|  |  | 
|  | return NULL; | 
|  | } | 
|  |  | 
|  | static void remove_node(struct gcov_node *node); | 
|  |  | 
|  | /* | 
|  | * write() implementation for gcov data files. Reset profiling data for the | 
|  | * associated file. If the object file has been unloaded (i.e. this is | 
|  | * a "ghost" node), remove the debug fs node as well. | 
|  | */ | 
|  | static ssize_t gcov_seq_write(struct file *file, const char __user *addr, | 
|  | size_t len, loff_t *pos) | 
|  | { | 
|  | struct seq_file *seq; | 
|  | struct gcov_info *info; | 
|  | struct gcov_node *node; | 
|  |  | 
|  | seq = file->private_data; | 
|  | info = gcov_iter_get_info(seq->private); | 
|  | mutex_lock(&node_lock); | 
|  | node = get_node_by_name(info->filename); | 
|  | if (node) { | 
|  | /* Reset counts or remove node for unloaded modules. */ | 
|  | if (node->ghost) | 
|  | remove_node(node); | 
|  | else | 
|  | gcov_info_reset(node->info); | 
|  | } | 
|  | /* Reset counts for open file. */ | 
|  | gcov_info_reset(info); | 
|  | mutex_unlock(&node_lock); | 
|  |  | 
|  | return len; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Given a string <path> representing a file path of format: | 
|  | *   path/to/file.gcda | 
|  | * construct and return a new string: | 
|  | *   <dir/>path/to/file.<ext> | 
|  | */ | 
|  | static char *link_target(const char *dir, const char *path, const char *ext) | 
|  | { | 
|  | char *target; | 
|  | char *old_ext; | 
|  | char *copy; | 
|  |  | 
|  | copy = kstrdup(path, GFP_KERNEL); | 
|  | if (!copy) | 
|  | return NULL; | 
|  | old_ext = strrchr(copy, '.'); | 
|  | if (old_ext) | 
|  | *old_ext = '\0'; | 
|  | if (dir) | 
|  | target = kasprintf(GFP_KERNEL, "%s/%s.%s", dir, copy, ext); | 
|  | else | 
|  | target = kasprintf(GFP_KERNEL, "%s.%s", copy, ext); | 
|  | kfree(copy); | 
|  |  | 
|  | return target; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Construct a string representing the symbolic link target for the given | 
|  | * gcov data file name and link type. Depending on the link type and the | 
|  | * location of the data file, the link target can either point to a | 
|  | * subdirectory of srctree, objtree or in an external location. | 
|  | */ | 
|  | static char *get_link_target(const char *filename, const struct gcov_link *ext) | 
|  | { | 
|  | const char *rel; | 
|  | char *result; | 
|  |  | 
|  | if (strncmp(filename, objtree, strlen(objtree)) == 0) { | 
|  | rel = filename + strlen(objtree) + 1; | 
|  | if (ext->dir == SRC_TREE) | 
|  | result = link_target(srctree, rel, ext->ext); | 
|  | else | 
|  | result = link_target(objtree, rel, ext->ext); | 
|  | } else { | 
|  | /* External compilation. */ | 
|  | result = link_target(NULL, filename, ext->ext); | 
|  | } | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | #define SKEW_PREFIX	".tmp_" | 
|  |  | 
|  | /* | 
|  | * For a filename .tmp_filename.ext return filename.ext. Needed to compensate | 
|  | * for filename skewing caused by the mod-versioning mechanism. | 
|  | */ | 
|  | static const char *deskew(const char *basename) | 
|  | { | 
|  | if (strncmp(basename, SKEW_PREFIX, sizeof(SKEW_PREFIX) - 1) == 0) | 
|  | return basename + sizeof(SKEW_PREFIX) - 1; | 
|  | return basename; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Create links to additional files (usually .c and .gcno files) which the | 
|  | * gcov tool expects to find in the same directory as the gcov data file. | 
|  | */ | 
|  | static void add_links(struct gcov_node *node, struct dentry *parent) | 
|  | { | 
|  | char *basename; | 
|  | char *target; | 
|  | int num; | 
|  | int i; | 
|  |  | 
|  | for (num = 0; gcov_link[num].ext; num++) | 
|  | /* Nothing. */; | 
|  | node->links = kcalloc(num, sizeof(struct dentry *), GFP_KERNEL); | 
|  | if (!node->links) | 
|  | return; | 
|  | for (i = 0; i < num; i++) { | 
|  | target = get_link_target(get_node_info(node)->filename, | 
|  | &gcov_link[i]); | 
|  | if (!target) | 
|  | goto out_err; | 
|  | basename = strrchr(target, '/'); | 
|  | if (!basename) | 
|  | goto out_err; | 
|  | basename++; | 
|  | node->links[i] = debugfs_create_symlink(deskew(basename), | 
|  | parent,	target); | 
|  | if (!node->links[i]) | 
|  | goto out_err; | 
|  | kfree(target); | 
|  | } | 
|  |  | 
|  | return; | 
|  | out_err: | 
|  | kfree(target); | 
|  | while (i-- > 0) | 
|  | debugfs_remove(node->links[i]); | 
|  | kfree(node->links); | 
|  | node->links = NULL; | 
|  | } | 
|  |  | 
|  | static const struct file_operations gcov_data_fops = { | 
|  | .open		= gcov_seq_open, | 
|  | .release	= gcov_seq_release, | 
|  | .read		= seq_read, | 
|  | .llseek		= seq_lseek, | 
|  | .write		= gcov_seq_write, | 
|  | }; | 
|  |  | 
|  | /* Basic initialization of a new node. */ | 
|  | static void init_node(struct gcov_node *node, struct gcov_info *info, | 
|  | const char *name, struct gcov_node *parent) | 
|  | { | 
|  | INIT_LIST_HEAD(&node->list); | 
|  | INIT_LIST_HEAD(&node->children); | 
|  | INIT_LIST_HEAD(&node->all); | 
|  | node->info = info; | 
|  | node->parent = parent; | 
|  | if (name) | 
|  | strcpy(node->name, name); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Create a new node and associated debugfs entry. Needs to be called with | 
|  | * node_lock held. | 
|  | */ | 
|  | static struct gcov_node *new_node(struct gcov_node *parent, | 
|  | struct gcov_info *info, const char *name) | 
|  | { | 
|  | struct gcov_node *node; | 
|  |  | 
|  | node = kzalloc(sizeof(struct gcov_node) + strlen(name) + 1, GFP_KERNEL); | 
|  | if (!node) { | 
|  | pr_warning("out of memory\n"); | 
|  | return NULL; | 
|  | } | 
|  | init_node(node, info, name, parent); | 
|  | /* Differentiate between gcov data file nodes and directory nodes. */ | 
|  | if (info) { | 
|  | node->dentry = debugfs_create_file(deskew(node->name), 0600, | 
|  | parent->dentry, node, &gcov_data_fops); | 
|  | } else | 
|  | node->dentry = debugfs_create_dir(node->name, parent->dentry); | 
|  | if (!node->dentry) { | 
|  | pr_warning("could not create file\n"); | 
|  | kfree(node); | 
|  | return NULL; | 
|  | } | 
|  | if (info) | 
|  | add_links(node, parent->dentry); | 
|  | list_add(&node->list, &parent->children); | 
|  | list_add(&node->all, &all_head); | 
|  |  | 
|  | return node; | 
|  | } | 
|  |  | 
|  | /* Remove symbolic links associated with node. */ | 
|  | static void remove_links(struct gcov_node *node) | 
|  | { | 
|  | int i; | 
|  |  | 
|  | if (!node->links) | 
|  | return; | 
|  | for (i = 0; gcov_link[i].ext; i++) | 
|  | debugfs_remove(node->links[i]); | 
|  | kfree(node->links); | 
|  | node->links = NULL; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Remove node from all lists and debugfs and release associated resources. | 
|  | * Needs to be called with node_lock held. | 
|  | */ | 
|  | static void release_node(struct gcov_node *node) | 
|  | { | 
|  | list_del(&node->list); | 
|  | list_del(&node->all); | 
|  | debugfs_remove(node->dentry); | 
|  | remove_links(node); | 
|  | if (node->ghost) | 
|  | gcov_info_free(node->ghost); | 
|  | kfree(node); | 
|  | } | 
|  |  | 
|  | /* Release node and empty parents. Needs to be called with node_lock held. */ | 
|  | static void remove_node(struct gcov_node *node) | 
|  | { | 
|  | struct gcov_node *parent; | 
|  |  | 
|  | while ((node != &root_node) && list_empty(&node->children)) { | 
|  | parent = node->parent; | 
|  | release_node(node); | 
|  | node = parent; | 
|  | } | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Find child node with given basename. Needs to be called with node_lock | 
|  | * held. | 
|  | */ | 
|  | static struct gcov_node *get_child_by_name(struct gcov_node *parent, | 
|  | const char *name) | 
|  | { | 
|  | struct gcov_node *node; | 
|  |  | 
|  | list_for_each_entry(node, &parent->children, list) { | 
|  | if (strcmp(node->name, name) == 0) | 
|  | return node; | 
|  | } | 
|  |  | 
|  | return NULL; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * write() implementation for reset file. Reset all profiling data to zero | 
|  | * and remove ghost nodes. | 
|  | */ | 
|  | static ssize_t reset_write(struct file *file, const char __user *addr, | 
|  | size_t len, loff_t *pos) | 
|  | { | 
|  | struct gcov_node *node; | 
|  |  | 
|  | mutex_lock(&node_lock); | 
|  | restart: | 
|  | list_for_each_entry(node, &all_head, all) { | 
|  | if (node->info) | 
|  | gcov_info_reset(node->info); | 
|  | else if (list_empty(&node->children)) { | 
|  | remove_node(node); | 
|  | /* Several nodes may have gone - restart loop. */ | 
|  | goto restart; | 
|  | } | 
|  | } | 
|  | mutex_unlock(&node_lock); | 
|  |  | 
|  | return len; | 
|  | } | 
|  |  | 
|  | /* read() implementation for reset file. Unused. */ | 
|  | static ssize_t reset_read(struct file *file, char __user *addr, size_t len, | 
|  | loff_t *pos) | 
|  | { | 
|  | /* Allow read operation so that a recursive copy won't fail. */ | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct file_operations gcov_reset_fops = { | 
|  | .write	= reset_write, | 
|  | .read	= reset_read, | 
|  | }; | 
|  |  | 
|  | /* | 
|  | * Create a node for a given profiling data set and add it to all lists and | 
|  | * debugfs. Needs to be called with node_lock held. | 
|  | */ | 
|  | static void add_node(struct gcov_info *info) | 
|  | { | 
|  | char *filename; | 
|  | char *curr; | 
|  | char *next; | 
|  | struct gcov_node *parent; | 
|  | struct gcov_node *node; | 
|  |  | 
|  | filename = kstrdup(info->filename, GFP_KERNEL); | 
|  | if (!filename) | 
|  | return; | 
|  | parent = &root_node; | 
|  | /* Create directory nodes along the path. */ | 
|  | for (curr = filename; (next = strchr(curr, '/')); curr = next + 1) { | 
|  | if (curr == next) | 
|  | continue; | 
|  | *next = 0; | 
|  | if (strcmp(curr, ".") == 0) | 
|  | continue; | 
|  | if (strcmp(curr, "..") == 0) { | 
|  | if (!parent->parent) | 
|  | goto err_remove; | 
|  | parent = parent->parent; | 
|  | continue; | 
|  | } | 
|  | node = get_child_by_name(parent, curr); | 
|  | if (!node) { | 
|  | node = new_node(parent, NULL, curr); | 
|  | if (!node) | 
|  | goto err_remove; | 
|  | } | 
|  | parent = node; | 
|  | } | 
|  | /* Create file node. */ | 
|  | node = new_node(parent, info, curr); | 
|  | if (!node) | 
|  | goto err_remove; | 
|  | out: | 
|  | kfree(filename); | 
|  | return; | 
|  |  | 
|  | err_remove: | 
|  | remove_node(parent); | 
|  | goto out; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * The profiling data set associated with this node is being unloaded. Store a | 
|  | * copy of the profiling data and turn this node into a "ghost". | 
|  | */ | 
|  | static int ghost_node(struct gcov_node *node) | 
|  | { | 
|  | node->ghost = gcov_info_dup(node->info); | 
|  | if (!node->ghost) { | 
|  | pr_warning("could not save data for '%s' (out of memory)\n", | 
|  | node->info->filename); | 
|  | return -ENOMEM; | 
|  | } | 
|  | node->info = NULL; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Profiling data for this node has been loaded again. Add profiling data | 
|  | * from previous instantiation and turn this node into a regular node. | 
|  | */ | 
|  | static void revive_node(struct gcov_node *node, struct gcov_info *info) | 
|  | { | 
|  | if (gcov_info_is_compatible(node->ghost, info)) | 
|  | gcov_info_add(info, node->ghost); | 
|  | else { | 
|  | pr_warning("discarding saved data for '%s' (version changed)\n", | 
|  | info->filename); | 
|  | } | 
|  | gcov_info_free(node->ghost); | 
|  | node->ghost = NULL; | 
|  | node->info = info; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Callback to create/remove profiling files when code compiled with | 
|  | * -fprofile-arcs is loaded/unloaded. | 
|  | */ | 
|  | void gcov_event(enum gcov_action action, struct gcov_info *info) | 
|  | { | 
|  | struct gcov_node *node; | 
|  |  | 
|  | mutex_lock(&node_lock); | 
|  | node = get_node_by_name(info->filename); | 
|  | switch (action) { | 
|  | case GCOV_ADD: | 
|  | /* Add new node or revive ghost. */ | 
|  | if (!node) { | 
|  | add_node(info); | 
|  | break; | 
|  | } | 
|  | if (gcov_persist) | 
|  | revive_node(node, info); | 
|  | else { | 
|  | pr_warning("could not add '%s' (already exists)\n", | 
|  | info->filename); | 
|  | } | 
|  | break; | 
|  | case GCOV_REMOVE: | 
|  | /* Remove node or turn into ghost. */ | 
|  | if (!node) { | 
|  | pr_warning("could not remove '%s' (not found)\n", | 
|  | info->filename); | 
|  | break; | 
|  | } | 
|  | if (gcov_persist) { | 
|  | if (!ghost_node(node)) | 
|  | break; | 
|  | } | 
|  | remove_node(node); | 
|  | break; | 
|  | } | 
|  | mutex_unlock(&node_lock); | 
|  | } | 
|  |  | 
|  | /* Create debugfs entries. */ | 
|  | static __init int gcov_fs_init(void) | 
|  | { | 
|  | int rc = -EIO; | 
|  |  | 
|  | init_node(&root_node, NULL, NULL, NULL); | 
|  | /* | 
|  | * /sys/kernel/debug/gcov will be parent for the reset control file | 
|  | * and all profiling files. | 
|  | */ | 
|  | root_node.dentry = debugfs_create_dir("gcov", NULL); | 
|  | if (!root_node.dentry) | 
|  | goto err_remove; | 
|  | /* | 
|  | * Create reset file which resets all profiling counts when written | 
|  | * to. | 
|  | */ | 
|  | reset_dentry = debugfs_create_file("reset", 0600, root_node.dentry, | 
|  | NULL, &gcov_reset_fops); | 
|  | if (!reset_dentry) | 
|  | goto err_remove; | 
|  | /* Replay previous events to get our fs hierarchy up-to-date. */ | 
|  | gcov_enable_events(); | 
|  | return 0; | 
|  |  | 
|  | err_remove: | 
|  | pr_err("init failed\n"); | 
|  | if (root_node.dentry) | 
|  | debugfs_remove(root_node.dentry); | 
|  |  | 
|  | return rc; | 
|  | } | 
|  | device_initcall(gcov_fs_init); |