/*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2008 Nokia Corporation * All rights reserved. * * This software was developed by Attilio Rao for the IPSO project under * contract to Nokia Corporation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice unmodified, this list of conditions, and the following * disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ #include <sys/cdefs.h> #include <sys/param.h> #include <sys/queue.h> #include <ctype.h> #include <paths.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> /* NB: Make sure FNBUFF is as large as LNBUFF, otherwise it could overflow */ #define FNBUFF 512 #define LNBUFF 512 #define TMPNAME "pmcannotate.XXXXXX" #define FATAL(ptr, x ...) do { \ fqueue_deleteall(); \ general_deleteall(); \ if ((ptr) != NULL) \ perror(ptr); \ fprintf(stderr, ##x); \ remove(tbfl); \ remove(tofl); \ exit(EXIT_FAILURE); \ } while (0) #define PERCSAMP(x) ((x) * 100 / totalsamples) struct entry { TAILQ_ENTRY(entry) en_iter; char *en_name; uintptr_t en_pc; uintptr_t en_ostart; uintptr_t en_oend; u_int en_nsamples; }; struct aggent { TAILQ_ENTRY(aggent) ag_fiter; long ag_offset; uintptr_t ag_ostart; uintptr_t ag_oend; char *ag_name; u_int ag_nsamples; }; static struct aggent *agg_create(const char *name, u_int nsamples, uintptr_t start, uintptr_t end); static void agg_destroy(struct aggent *agg) __unused; static void asmparse(FILE *fp); static int cparse(FILE *fp); static void entry_acqref(struct entry *entry); static struct entry *entry_create(const char *name, uintptr_t pc, uintptr_t start, uintptr_t end); static void entry_destroy(struct entry *entry) __unused; static void fqueue_compact(float th); static void fqueue_deleteall(void); static struct aggent *fqueue_findent_by_name(const char *name); static int fqueue_getall(const char *bin, char *temp, int asmf); static int fqueue_insertent(struct entry *entry); static int fqueue_insertgen(void); static void general_deleteall(void); static struct entry *general_findent(uintptr_t pc); static void general_insertent(struct entry *entry); static void general_printasm(FILE *fp, struct aggent *agg); static int general_printc(FILE *fp, struct aggent *agg); static int printblock(FILE *fp, struct aggent *agg); static void usage(const char *progname) __dead2; static TAILQ_HEAD(, entry) mainlst = TAILQ_HEAD_INITIALIZER(mainlst); static TAILQ_HEAD(, aggent) fqueue = TAILQ_HEAD_INITIALIZER(fqueue); /* * Use a float value in order to automatically promote operations * to return a float value rather than use casts. */ static float totalsamples; /* * Identifies a string cointaining objdump's assembly printout. */ static inline int isasminline(const char *str) { void *ptr; int nbytes; if (sscanf(str, " %p%n", &ptr, &nbytes) != 1) return (0); if (str[nbytes] != ':' || isspace(str[nbytes + 1]) == 0) return (0); return (1); } /* * Identifies a string containing objdump's assembly printout * for a new function. */ static inline int newfunction(const char *str) { char fname[FNBUFF]; void *ptr; int nbytes; if (isspace(str[0])) return (0); if (sscanf(str, "%p <%[^>:]>:%n", &ptr, fname, &nbytes) != 2) return (0); return (nbytes); } /* * Create a new first-level aggregation object for a specified * function. */ static struct aggent * agg_create(const char *name, u_int nsamples, uintptr_t start, uintptr_t end) { struct aggent *agg; agg = calloc(1, sizeof(struct aggent)); if (agg == NULL) return (NULL); agg->ag_name = strdup(name); if (agg->ag_name == NULL) { free(agg); return (NULL); } agg->ag_nsamples = nsamples; agg->ag_ostart = start; agg->ag_oend = end; return (agg); } /* * Destroy a first-level aggregation object for a specified * function. */ static void agg_destroy(struct aggent *agg) { free(agg->ag_name); free(agg); } /* * Analyze the "objdump -d" output, locate functions and start * printing out the assembly functions content. * We do not use newfunction() because we actually need the * function name in available form, but the heurstic used is * the same. */ static void asmparse(FILE *fp) { char buffer[LNBUFF], fname[FNBUFF]; struct aggent *agg; void *ptr; while (fgets(buffer, LNBUFF, fp) != NULL) { if (isspace(buffer[0])) continue; if (sscanf(buffer, "%p <%[^>:]>:", &ptr, fname) != 2) continue; agg = fqueue_findent_by_name(fname); if (agg == NULL) continue; agg->ag_offset = ftell(fp); } TAILQ_FOREACH(agg, &fqueue, ag_fiter) { if (fseek(fp, agg->ag_offset, SEEK_SET) == -1) return; printf("Profile trace for function: %s() [%.2f%%]\n", agg->ag_name, PERCSAMP(agg->ag_nsamples)); general_printasm(fp, agg); printf("\n"); } } /* * Analyze the "objdump -S" output, locate functions and start * printing out the C functions content. * We do not use newfunction() because we actually need the * function name in available form, but the heurstic used is * the same. * In order to maintain the printout sorted, on the first pass it * simply stores the file offsets in order to fastly moved later * (when the file is hot-cached also) when the real printout will * happen. */ static int cparse(FILE *fp) { char buffer[LNBUFF], fname[FNBUFF]; struct aggent *agg; void *ptr; while (fgets(buffer, LNBUFF, fp) != NULL) { if (isspace(buffer[0])) continue; if (sscanf(buffer, "%p <%[^>:]>:", &ptr, fname) != 2) continue; agg = fqueue_findent_by_name(fname); if (agg == NULL) continue; agg->ag_offset = ftell(fp); } TAILQ_FOREACH(agg, &fqueue, ag_fiter) { if (fseek(fp, agg->ag_offset, SEEK_SET) == -1) return (-1); printf("Profile trace for function: %s() [%.2f%%]\n", agg->ag_name, PERCSAMP(agg->ag_nsamples)); if (general_printc(fp, agg) == -1) return (-1); printf("\n"); } return (0); } /* * Bump the number of samples for any raw entry. */ static void entry_acqref(struct entry *entry) { entry->en_nsamples++; } /* * Create a new raw entry object for a specified function. */ static struct entry * entry_create(const char *name, uintptr_t pc, uintptr_t start, uintptr_t end) { struct entry *obj; obj = calloc(1, sizeof(struct entry)); if (obj == NULL) return (NULL); obj->en_name = strdup(name); if (obj->en_name == NULL) { free(obj); return (NULL); } obj->en_pc = pc; obj->en_ostart = start; obj->en_oend = end; obj->en_nsamples = 1; return (obj); } /* * Destroy a raw entry object for a specified function. */ static void entry_destroy(struct entry *entry) { free(entry->en_name); free(entry); } /* * Specify a lower bound in percentage and drop from the * first-level aggregation queue all the objects with a * smaller impact. */ static void fqueue_compact(float th) { u_int thi; struct aggent *agg, *tmpagg; if (totalsamples == 0) return; /* Revert the percentage calculation. */ thi = th * totalsamples / 100; TAILQ_FOREACH_SAFE(agg, &fqueue, ag_fiter, tmpagg) if (agg->ag_nsamples < thi) TAILQ_REMOVE(&fqueue, agg, ag_fiter); } /* * Flush the first-level aggregates queue. */ static void fqueue_deleteall(void) { struct aggent *agg; while (TAILQ_EMPTY(&fqueue) == 0) { agg = TAILQ_FIRST(&fqueue); TAILQ_REMOVE(&fqueue, agg, ag_fiter); } } /* * Insert a raw entry into the aggregations queue. * If the respective first-level aggregation object * does not exist create it and maintain it sorted * in respect of the number of samples. */ static int fqueue_insertent(struct entry *entry) { struct aggent *obj, *tmp; int found; found = 0; TAILQ_FOREACH(obj, &fqueue, ag_fiter) if (!strcmp(obj->ag_name, entry->en_name)) { found = 1; obj->ag_nsamples += entry->en_nsamples; break; } /* * If the first-level aggregation object already exists, * just aggregate the samples and, if needed, resort * it. */ if (found) { TAILQ_REMOVE(&fqueue, obj, ag_fiter); found = 0; TAILQ_FOREACH(tmp, &fqueue, ag_fiter) if (obj->ag_nsamples > tmp->ag_nsamples) { found = 1; break; } if (found) TAILQ_INSERT_BEFORE(tmp, obj, ag_fiter); else TAILQ_INSERT_TAIL(&fqueue, obj, ag_fiter); return (0); } /* * If the first-level aggregation object does not * exist, create it and put in the sorted queue. * If this is the first object, we need to set the * head of the queue. */ obj = agg_create(entry->en_name, entry->en_nsamples, entry->en_ostart, entry->en_oend); if (obj == NULL) return (-1); if (TAILQ_EMPTY(&fqueue) != 0) { TAILQ_INSERT_HEAD(&fqueue, obj, ag_fiter); return (0); } TAILQ_FOREACH(tmp, &fqueue, ag_fiter) if (obj->ag_nsamples > tmp->ag_nsamples) { found = 1; break; } if (found) TAILQ_INSERT_BEFORE(tmp, obj, ag_fiter); else TAILQ_INSERT_TAIL(&fqueue, obj, ag_fiter); return (0); } /* * Lookup a first-level aggregation object by name. */ static struct aggent * fqueue_findent_by_name(const char *name) { struct aggent *obj; TAILQ_FOREACH(obj, &fqueue, ag_fiter) if (!strcmp(obj->ag_name, name)) return (obj); return (NULL); } /* * Return the number of object in the first-level aggregations queue. */ static int fqueue_getall(const char *bin, char *temp, int asmf) { char tmpf[MAXPATHLEN * 2 + 50]; struct aggent *agg; uintptr_t start, end; if (mkstemp(temp) == -1) return (-1); TAILQ_FOREACH(agg, &fqueue, ag_fiter) { bzero(tmpf, sizeof(tmpf)); start = agg->ag_ostart; end = agg->ag_oend; if (asmf) snprintf(tmpf, sizeof(tmpf), "objdump --start-address=%p " "--stop-address=%p -d %s >> %s", (void *)start, (void *)end, bin, temp); else snprintf(tmpf, sizeof(tmpf), "objdump --start-address=%p " "--stop-address=%p -S %s >> %s", (void *)start, (void *)end, bin, temp); if (system(tmpf) != 0) return (-1); } return (0); } /* * Insert all the raw entries present in the general queue * into the first-level aggregations queue. */ static int fqueue_insertgen(void) { struct entry *obj; TAILQ_FOREACH(obj, &mainlst, en_iter) if (fqueue_insertent(obj) == -1) return (-1); return (0); } /* * Flush the raw entries general queue. */ static void general_deleteall(void) { struct entry *obj; while (TAILQ_EMPTY(&mainlst) == 0) { obj = TAILQ_FIRST(&mainlst); TAILQ_REMOVE(&mainlst, obj, en_iter); } } /* * Lookup a raw entry by the PC. */ static struct entry * general_findent(uintptr_t pc) { struct entry *obj; TAILQ_FOREACH(obj, &mainlst, en_iter) if (obj->en_pc == pc) return (obj); return (NULL); } /* * Insert a new raw entry in the general queue. */ static void general_insertent(struct entry *entry) { TAILQ_INSERT_TAIL(&mainlst, entry, en_iter); } /* * Printout the body of an "objdump -d" assembly function. * It does simply stops when a new function is encountered, * bringing back the file position in order to not mess up * subsequent analysis. * C lines and others not recognized are simply skipped. */ static void general_printasm(FILE *fp, struct aggent *agg) { char buffer[LNBUFF]; struct entry *obj; int nbytes; void *ptr; while (fgets(buffer, LNBUFF, fp) != NULL) { if ((nbytes = newfunction(buffer)) != 0) { fseek(fp, nbytes * -1, SEEK_CUR); break; } if (!isasminline(buffer)) continue; if (sscanf(buffer, " %p:", &ptr) != 1) continue; obj = general_findent((uintptr_t)ptr); if (obj == NULL) printf("\t| %s", buffer); else printf("%.2f%%\t| %s", (float)obj->en_nsamples * 100 / agg->ag_nsamples, buffer); } } /* * Printout the body of an "objdump -S" function. * It does simply stops when a new function is encountered, * bringing back the file position in order to not mess up * subsequent analysis. * It expect from the starting to the end to find, always, valid blocks * (see below for an explanation of the "block" concept). */ static int general_printc(FILE *fp, struct aggent *agg) { char buffer[LNBUFF]; while (fgets(buffer, LNBUFF, fp) != NULL) { fseek(fp, strlen(buffer) * -1, SEEK_CUR); if (newfunction(buffer) != 0) break; if (printblock(fp, agg) == -1) return (-1); } return (0); } /* * Printout a single block inside an "objdump -S" function. * The block is composed of a first part in C and subsequent translation * in assembly. * This code also operates a second-level aggregation packing together * samples relative to PCs into a (lower bottom) block with their * C (higher half) counterpart. */ static int printblock(FILE *fp, struct aggent *agg) { char buffer[LNBUFF]; long lstart; struct entry *obj; u_int tnsamples; int done, nbytes, sentinel; void *ptr; /* * We expect the first thing of the block is C code, so simply give * up if asm line is found. */ lstart = ftell(fp); sentinel = 0; for (;;) { if (fgets(buffer, LNBUFF, fp) == NULL) return (0); if (isasminline(buffer) != 0) break; sentinel = 1; nbytes = newfunction(buffer); if (nbytes != 0) { if (fseek(fp, nbytes * -1, SEEK_CUR) == -1) return (-1); return (0); } } /* * If the sentinel is not set, it means it did not match any * "high half" for this code so simply give up. * Operates the second-level aggregation. */ tnsamples = 0; do { if (sentinel == 0) return (-1); if (sscanf(buffer, " %p:", &ptr) != 1) return (-1); obj = general_findent((uintptr_t)ptr); if (obj != NULL) tnsamples += obj->en_nsamples; } while (fgets(buffer, LNBUFF, fp) != NULL && isasminline(buffer) != 0); /* Rewind to the start of the block in order to start the printout. */ if (fseek(fp, lstart, SEEK_SET) == -1) return (-1); /* Again the high half of the block rappresenting the C part. */ done = 0; while (fgets(buffer, LNBUFF, fp) != NULL && isasminline(buffer) == 0) { if (tnsamples == 0 || done != 0) printf("\t| %s", buffer); else { done = 1; printf("%.2f%%\t| %s", (float)tnsamples * 100 / agg->ag_nsamples, buffer); } } /* * Again the low half of the block rappresenting the asm * translation part. */ for (;;) { if (fgets(buffer, LNBUFF, fp) == NULL) return (0); if (isasminline(buffer) == 0) break; nbytes = newfunction(buffer); if (nbytes != 0) { if (fseek(fp, nbytes * -1, SEEK_CUR) == -1) return (-1); return (0); } } if (fseek(fp, strlen(buffer) * -1, SEEK_CUR) == -1) return (-1); return (0); } /* * Helper printout functions. */ static void usage(const char *progname) { fprintf(stderr, "usage: %s [-a] [-h] [-k kfile] [-l lb] pmcraw.out binary\n", progname); exit(EXIT_SUCCESS); } int main(int argc, char *argv[]) { char buffer[LNBUFF], fname[FNBUFF]; char *tbfl, *tofl, *tmpdir; char tmpf[MAXPATHLEN * 2 + 50]; float limit; char *bin, *exec, *kfile, *ofile; struct entry *obj; FILE *gfp, *bfp; void *ptr, *hstart, *hend; uintptr_t tmppc, ostart, oend; int cget, asmsrc; exec = argv[0]; ofile = NULL; bin = NULL; kfile = NULL; asmsrc = 0; limit = 0.5; while ((cget = getopt(argc, argv, "ahl:k:")) != -1) switch(cget) { case 'a': asmsrc = 1; break; case 'k': kfile = optarg; break; case 'l': limit = (float)atof(optarg); break; case 'h': case '?': default: usage(exec); } argc -= optind; argv += optind; if (argc != 2) usage(exec); ofile = argv[0]; bin = argv[1]; if (access(bin, R_OK | F_OK) == -1) FATAL(exec, "%s: Impossible to locate the binary file\n", exec); if (access(ofile, R_OK | F_OK) == -1) FATAL(exec, "%s: Impossible to locate the pmcstat file\n", exec); if (kfile != NULL && access(kfile, R_OK | F_OK) == -1) FATAL(exec, "%s: Impossible to locate the kernel file\n", exec); bzero(tmpf, sizeof(tmpf)); tmpdir = getenv("TMPDIR"); if (tmpdir == NULL) { asprintf(&tbfl, "%s/%s", _PATH_TMP, TMPNAME); asprintf(&tofl, "%s/%s", _PATH_TMP, TMPNAME); } else { asprintf(&tbfl, "%s/%s", tmpdir, TMPNAME); asprintf(&tofl, "%s/%s", tmpdir, TMPNAME); } if (tofl == NULL || tbfl == NULL) FATAL(exec, "%s: Cannot create tempfile templates\n", exec); if (mkstemp(tofl) == -1) FATAL(exec, "%s: Impossible to create the tmp file\n", exec); if (kfile != NULL) snprintf(tmpf, sizeof(tmpf), "pmcstat -k %s -R %s -m %s", kfile, ofile, tofl); else snprintf(tmpf, sizeof(tmpf), "pmcstat -R %s -m %s", ofile, tofl); if (system(tmpf) != 0) FATAL(exec, "%s: Impossible to create the tmp file\n", exec); gfp = fopen(tofl, "r"); if (gfp == NULL) FATAL(exec, "%s: Impossible to open the map file\n", exec); /* * Make the collection of raw entries from a pmcstat mapped file. * The heuristic here wants strings in the form: * "addr funcname startfaddr endfaddr". */ while (fgets(buffer, LNBUFF, gfp) != NULL) { if (isspace(buffer[0])) continue; if (sscanf(buffer, "%p %s %p %p\n", &ptr, fname, &hstart, &hend) != 4) FATAL(NULL, "%s: Invalid scan of function in the map file\n", exec); ostart = (uintptr_t)hstart; oend = (uintptr_t)hend; tmppc = (uintptr_t)ptr; totalsamples++; obj = general_findent(tmppc); if (obj != NULL) { entry_acqref(obj); continue; } obj = entry_create(fname, tmppc, ostart, oend); if (obj == NULL) FATAL(exec, "%s: Impossible to create a new object\n", exec); general_insertent(obj); } if (fclose(gfp) == EOF) FATAL(exec, "%s: Impossible to close the filedesc\n", exec); if (remove(tofl) == -1) FATAL(exec, "%s: Impossible to remove the tmpfile\n", exec); /* * Remove the loose end objects and feed the first-level aggregation * queue. */ if (fqueue_insertgen() == -1) FATAL(exec, "%s: Impossible to generate an analysis\n", exec); fqueue_compact(limit); if (fqueue_getall(bin, tbfl, asmsrc) == -1) FATAL(exec, "%s: Impossible to create the tmp file\n", exec); bfp = fopen(tbfl, "r"); if (bfp == NULL) FATAL(exec, "%s: Impossible to open the binary file\n", exec); if (asmsrc != 0) asmparse(bfp); else if (cparse(bfp) == -1) FATAL(NULL, "%s: Invalid format for the C file\n", exec); if (fclose(bfp) == EOF) FATAL(exec, "%s: Impossible to close the filedesc\n", exec); if (remove(tbfl) == -1) FATAL(exec, "%s: Impossible to remove the tmpfile\n", exec); return (0); }