Как определить количество машинных инструкций x86, выполняемых в программе на C?

В настоящее время я работаю над проблемой домашнего задания, которая просит меня узнать количество инструкций машинного кода, которые выполняются при запуске короткой программы, которую я написал на C.

В вопросе говорится, что я могу использовать любые инструменты, которые хочу выяснить, но я довольно плохо знаком с C и очень мало знаю, как это сделать.

Какие типы инструментов мне нужны, чтобы понять это?

4 ответа

Терминология: вы запрашиваете динамический счетчик команд. например, подсчет инструкции внутри цикла каждый раз, когда она выполняется. Обычно это примерно связано с производительностью, но количество команд за цикл может сильно отличаться.

Что-то, на что люди также обращают внимание, - это статический счетчик команд (или, чаще всего, просто размер кода, потому что это то, что действительно имеет значение для объема кэша команд и времени загрузки диска). Для наборов команд переменной длины, таких как x86, они коррелированы, но это не одно и то же. На RISC с инструкциями фиксированной длины, например MIPS или AArch64, он ближе, но у вас все еще есть заполнение, например, для выравнивания начала функций. Это совершенно отдельный показатель. gcc -Os оптимизирует размер кода, стараясь не жертвовать большой скоростью.


Если вы используете Linux, используйте gcc -O2 foo.c скомпилировать ваш код. -O2 не включает автоматическую векторизацию для gcc. (Это для лязг). Вероятно, это хороший базовый уровень оптимизации, который избавит вас от того, что в вашем C-коде не требуется, чтобы избежать глупых различий между использованием большего или меньшего числа переменных tmp для разбивки большого выражения. Может быть, использовать -Og если вы хотите минимальную оптимизацию, или -O0 если вам нужен действительно тупой код braindead, который компилирует каждый оператор отдельно и никогда не хранит ничего в регистрах между операторами. ( Почему clang создает неэффективный asm с -O0 (для этой простой суммы с плавающей запятой)?).

Да, очень важно, как вы компилируете. gcc -O3 -march=native -ffast-mathможет использовать намного меньше инструкций, если он автоматически векторизует цикл.

Чтобы остановить код от оптимизации, возьмите входные данные из аргумента командной строки или прочитайте его изvolatileпеременная. подобноvolatile int size_volatile = 1234;int size = size_volatile;, И вернуть или напечатать результат, потому что, если у программы нет побочных эффектов, тогда наиболее эффективная реализация - просто выйти немедленно.


Тогда бегиperf stat ./a.out, При этом будут использоваться счетчики производительности оборудования, чтобы дать вам общее количество инструкций, выполненных от имени вашего процесса, в том числе внутри ядра. (Наряду с другими счетчиками, такими как тактовые частоты ядра процессора, и некоторыми программными счетчиками, такими какpage-faultsи время в микросекундах.)

Чтобы считать только инструкции пользовательского пространства, используйте perf stat -e instructions:u ./a.out, Это все еще будет очень большое число даже для простой программы "hello world", такой как 180k, потому что она включает в себя запуск динамического компоновщика и весь код, который выполняется внутри библиотечных функций. И код запуска ЭЛТ, который вызывает вашmainи это делаетexitсистемный вызов с mainвозвращаемое значение, если вы возвращаете вместо вызоваexit(3),

Вы можете статически связать свою программу на C, чтобы уменьшить эти накладные расходы при компиляции сgcc -O2 -static -fno-stack-protector -fno-pie -no-pie

perfподсчетinstructions:uкажется довольно точным на моем процессоре Skylake. Статически связанный двоичный файл x86-64, который содержит только 2 инструкции,mov eax, 231/syscall, считается как 3 инструкции. Возможно, при переходе между ядром и пользовательским режимом учитывается одна дополнительная инструкция, но она довольно незначительна.

$ perf stat -e instructions:u ./exit    # hand-written in asm to check for perf overhead

 Performance counter stats for './exit':

                 3      instructions:u                                              

       0.000651529 seconds time elapsed

Статически связанный двоичный файл, который вызываетputsдважды считается33,202 instructions:uсоставлено сgcc -O2 -static -fno-stack-protector -fno-pie -no-pie hello.c, Кажется разумным для функций инициализации glibc, включая stdio и запуск CRT перед вызовомmain, (main само по себе имеет только 8 инструкций, которые я проверил с objdump -drwC -Mintel a.out | less).


Другие источники:

  • Количество выполненных Инструкций отличается для программы Hello World Nasm Assembly и C

    Ответ @MichaelPetch показывает, как использовать альтернативный libc (MUSL), для запуска которого не требуется код запуска.printfработать. Таким образом, вы можете скомпилировать программу на C и установить ееmainв качестве точки входа ELF (и вызова_exit()вместо возвращения).

  • Как я могу профилировать код C++, работающий в Linux? Существуют тонны инструментов профилирования для поиска горячих точек и дорогостоящих функций (включая время, затрачиваемое на функции, которые они вызывают, то есть профилирование обратного стека). В основном речь идет не о подсчете инструкций.


Бинарные инструменты:

Это мощные инструменты для подсчета инструкций, включая подсчет только определенных видов инструкций.

  • Intel Pin - инструмент динамического бинарного инструментария
  • Эмулятор разработки программного обеспечения Intel® (SDE) Он основан на PIN-коде и удобен для таких вещей, как тестирование кода AVX512 на компьютере разработчика, который не поддерживает AVX512. (Он динамически перекомпилируется, поэтому большинство инструкций выполняется изначально, но неподдерживаемые инструкции вызывают процедуру эмуляции.)

    Например,sde64 -mix -- ./my_programнапечатает комбинацию команд для вашей программы с общим количеством для каждой инструкции и разбивкой по категориям. Посмотрите libsvm, скомпилированный с AVX против AVX для примера вида вывода.

    Это также дает вам таблицу общего количества динамических команд для каждой функции, а также для каждого потока и глобального.Вывод микширования SDE не очень хорошо работает на исполняемом файле PIE: он считает, что динамический компоновщик является исполняемым файлом (потому что он есть), поэтому скомпилируйте gcc -O2 -no-pie -fno-pie prog.c -o prog, Это до сих пор не вижуputs звонки илиmainсам в профиле для программы Wello World Test, и я не знаю, почему нет.

  • Расчет "FLOP" с помощью эмулятора разработки программного обеспечения Intel® (Intel® SDE) Пример использования SDE для подсчета определенных видов инструкций, таких какvfmadd231pd,

    Процессоры Intel имеют счетчики производительности HW для таких событий, какfp_arith_inst_retired.256b_packed_double, так что вы можете использовать их для подсчета FLOP. Они фактически считают FMA как 2 события. Так что, если у вас есть процессор Intel, который может выполнять ваш код изначально, вы можете сделать это вместо perf stat -e -e fp_arith_inst_retired.256b_packed_double,fp_arith_inst_retired.128b_packed_double,fp_arith_inst_retired.scalar_double, (И / или события для одинарной точности.)

    Но для большинства других видов инструкций нет событий, только математика FP.

Это все вещи Intel;IDK, что есть у AMD, или любой другой материал для ISA, кроме x86.Это только те инструменты, о которых я слышал;Я уверен, что есть много вещей, которые я опускаю.

Как я уже упоминал в моих главных комментариях, один из способов сделать это - написать программу, которая передает команды gdb,

В частности, si команда (пошаговая инструкция ISA).

Я не мог заставить это работать с трубами, но я смог заставить это работать, помещая gdb под псевдо-тты.

Редактировать: подумав, я придумал версию, которая использует ptrace непосредственно в целевой программе вместо отправки команд gdb, Это намного быстрее [в 100 раз быстрее] и [вероятно] более надежно


Итак, вот gdb основанная программа управления. Обратите внимание, что это должно быть связано с -lutil,

// gdbctl -- gdb control via pseudo tty

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <poll.h>
#include <pty.h>
#include <utmp.h>
#include <sys/types.h>
#include <sys/wait.h>

int opt_d;                              // 1=show debug output
int opt_e;                              // 1=echo gdb output
int opt_f;                              // 1=set line buffered output
int opt_x;                              // si repetition factor

int zpxlvl;                             // current trace level

int ptypar;                             // parent PTY fd
int ptycld;                             // child PTY fd
char name[100];                         // child PTY device name

unsigned long long sicount;             // single step count

const char *gdb = "(gdb) ";             // gdb's prompt string
const char *waitstr;                    // currently active "wait for" string
char *waitstop[8] = { NULL };           // string that shows run is done

int stopflg;                            // 1=waitstop seen
char sicmd[100];

char waitbuf[10000];                    // large buffer to scan for strings
char *waitdst = waitbuf;                // current end position

pid_t pidgdb;                           // gdb's pid
pid_t pidfin;                           // stop pid
int status;                             // gdb's final status
double tvelap;                          // start time

#ifndef _USE_ZPRT_
#define _USE_ZPRT_      1
#endif

static inline int
zprtok(int lvl)
{

    return (_USE_ZPRT_ && (opt_d >= lvl));
}

#define dbg(_lvl,_fmt...) \
    do { \
        if (zprtok(_lvl)) \
            printf(_fmt); \
    } while (0)

// tvgetf -- get high precision time
double
tvgetf(void)
{
    struct timespec ts;
    double sec;

    clock_gettime(CLOCK_REALTIME,&ts);
    sec = ts.tv_nsec;
    sec /= 1e9;
    sec += ts.tv_sec;

    return sec;
}

// xstrcat -- concatenate a string
char *
xstrcat(char *dst,const char *src)
{
    int chr;

    for (chr = *src++;  chr != 0;  chr = *src++)
        *dst++ = chr;

    *dst = 0;

    return dst;
}

// gdbexit -- check for gdb termination
void
gdbexit(void)
{

    // NOTE: this should _not_ happen
    do {
        if (pidgdb == 0)
            break;

        pidfin = waitpid(pidgdb,&status,WNOHANG);
        if (pidfin == 0)
            break;

        pidgdb = 0;
        printf("gdbexit: WAITPID status=%8.8X\n",status);
        exit(8);
    } while (0);
}

// gdbwaitpoll -- wait for prompt string
void
gdbwaitpoll(const char *buf)
{
    char *cp;
    char **wstr;

    do {
        gdbexit();

        if (waitstr == NULL)
            break;

        // concatenate to big buffer
        dbg(2,"BUF '%s'\n",buf);
        waitdst = xstrcat(waitdst,buf);

        // check for final termination string (e.g. "exited with")
        for (wstr = waitstop;  *wstr != NULL;  ++wstr) {
            cp = *wstr;
            dbg(2,"TRYSTOP '%s'\n",cp);
            cp = strstr(waitbuf,cp);
            if (cp != NULL) {
                stopflg = 1;
                waitstop[0] = NULL;
            }
        }

        // check for the prompt (e.g. "(gdb) ")
        cp = strstr(waitbuf,waitstr);
        if (cp == NULL)
            break;

        dbg(1,"HIT on '%s'\n",waitstr);

        // got it reset things
        waitbuf[0] = 0;
        waitdst = waitbuf;
        waitstr = NULL;
    } while (0);
}

// gdbrecv -- process input from gdb
void
gdbrecv(void)
{
    struct pollfd fds[1];
    struct pollfd *fd = &fds[0];
    int xlen;
    char buf[1000];

    fd->fd = ptypar;
    fd->events = POLLIN;

    while (1) {
        gdbexit();

#if 1
        int nfd = poll(fds,1,1);
        if (nfd <= 0) {
            if (waitstr != NULL)
                continue;
            break;
        }
#endif

        // get a chunk of data
        xlen = read(ptypar,buf,sizeof(buf) - 1);
        dbg(1,"gdbrecv: READ xlen=%d\n",xlen);

        if (xlen < 0) {
            printf("ERR: %s\n",strerror(errno));
            break;
        }

        // wait until we've drained every bit of data
        if (xlen == 0) {
            if (waitstr != NULL)
                continue;
            break;
        }

        // add EOS char
        buf[xlen] = 0;

        dbg(1,"ECHO: ");
        if (opt_e)
            fwrite(buf,1,xlen,stdout);

        // wait for our prompt
        gdbwaitpoll(buf);
    }
}

// gdbwaitfor -- set up prompt string to wait for
void
gdbwaitfor(const char *wstr,int loopflg)
{

    waitstr = wstr;
    if (waitstr != NULL)
        dbg(1,"WAITFOR: '%s'\n",waitstr);

    while ((waitstr != NULL) && loopflg && (pidgdb != 0))
        gdbrecv();
}

// gdbcmd -- send command to gdb
void
gdbcmd(const char *str,const char *wstr)
{
    int rlen = strlen(str);
    int xlen = 0;

#if 0
    printf("CMD/%d: %s",rlen,str);
#endif

    gdbwaitfor(wstr,0);

    for (;  rlen > 0;  rlen -= xlen, str += xlen) {
        gdbexit();

        xlen = write(ptypar,str,rlen);
        if (xlen <= 0)
            break;

        dbg(1,"RET: rlen=%d xlen=%d\n",rlen,xlen);
        gdbrecv();
    }

    dbg(1,"END/%d\n",xlen);
}

// gdbctl -- control gdb
void
gdbctl(int argc,char **argv)
{

    // this is the optimal number for speed
    if (opt_x < 0)
        opt_x = 100;

    if (opt_x <= 1) {
        opt_x = 1;
        sprintf(sicmd,"si\n");
    }
    else
        sprintf(sicmd,"si %d\n",opt_x);

    // create pseudo TTY
    openpty(&ptypar,&ptycld,name,NULL,NULL);

    pidgdb = fork();

    // launch gdb
    if (pidgdb == 0) {
        //sleep(1);

        login_tty(ptycld);
        close(ptypar);

        char *gargs[8];
        char **gdst = gargs;

        *gdst++ = "gdb";
        *gdst++ = "-n";
        *gdst++ = "-q";
        *gdst++ = *argv;
        *gdst = NULL;

        execvp(gargs[0],gargs);
        exit(9);
    }

    // make input from gdb non-blocking
#if 1
    int flags = fcntl(ptypar,F_GETFL,0);
    flags |= O_NONBLOCK;
    fcntl(ptypar,F_SETFL,flags);
#endif

    // wait
    char **wstr = waitstop;
    *wstr++ = "exited with code";
    *wstr++ = "Program received signal";
    *wstr++ = "Program terminated with signal";
    *wstr = NULL;

    printf("TTY: %s\n",name);
    printf("SI: %d\n",opt_x);
    printf("GDB: %d\n",pidgdb);

#if 1
    sleep(2);
#endif

    gdbwaitfor(gdb,1);

    // prevent kill or quit commands from hanging
    gdbcmd("set confirm off\n",gdb);

    // set breakpoint at earliest point
#if 1
    gdbcmd("b _start\n",gdb);
#else
    gdbcmd("b main\n",gdb);
#endif

    // skip over target program name
    --argc;
    ++argv;

    // add extra arguments
    do {
        if (argc <= 0)
            break;

        char xargs[1000];
        char *xdst = xargs;
        xdst += sprintf(xdst,"set args");

        for (int avidx = 0;  avidx < argc;  ++avidx, ++argv) {
            printf("XARGS: '%s'\n",*argv);
            xdst += sprintf(xdst," %s",*argv);
        }

        xdst += sprintf(xdst,"\n");

        gdbcmd(xargs,gdb);
    } while (0);

    // run the program -- it will stop at the breakpoint we set
    gdbcmd("run\n",gdb);

    // disable the breakpoint for speed
    gdbcmd("disable\n",gdb);

    tvelap = tvgetf();

    while (1) {
        // single step an ISA instruction
        gdbcmd(sicmd,gdb);

        // check for gdb aborting
        if (pidgdb == 0)
            break;

        // check for target program exiting
        if (stopflg)
            break;

        // advance count of ISA instructions
        sicount += opt_x;
    }

    // get elapsed time
    tvelap = tvgetf() - tvelap;

    // tell gdb to quit
    gdbcmd("quit\n",NULL);

    // wait for gdb to completely terminate
    if (pidgdb != 0) {
        pidfin = waitpid(pidgdb,&status,0);
        pidgdb = 0;
    }

    // close PTY units
    close(ptypar);
    close(ptycld);
}

// main -- main program
int
main(int argc,char **argv)
{
    char *cp;

    --argc;
    ++argv;

    for (;  argc > 0;  --argc, ++argv) {
        cp = *argv;
        if (*cp != '-')
            break;

        switch (cp[1]) {
        case 'd':
            cp += 2;
            opt_d = (*cp != 0) ? atoi(cp) : 1;
            break;

        case 'e':
            cp += 2;
            opt_e = (*cp != 0) ? atoi(cp) : 1;
            break;

        case 'f':
            cp += 2;
            opt_f = (*cp != 0) ? atoi(cp) : 1;
            break;

        case 'x':
            cp += 2;
            opt_x = (*cp != 0) ? atoi(cp) : -1;
            break;
        }
    }

    if (argc == 0) {
        printf("specify target program\n");
        exit(1);
    }

    // set output line buffering
    switch (opt_f) {
    case 0:
        break;

    case 1:
        setlinebuf(stdout);
        break;

    default:
        setbuf(stdout,NULL);
        break;
    }

    gdbctl(argc,argv);

    // print statistics
    printf("%llu instructions -- ELAPSED: %.9f -- %.3f insts / sec\n",
        sicount,tvelap,(double) sicount / tvelap);

    return 0;
}

Вот пример тестовой программы:

// tgt -- sample slave/test program

#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int opt_S;

int glob;

void
dumb(int x)
{

    glob += x;
}

int
spin(int lim)
{
    int x;

    for (x = 0;  x < lim;  ++x)
        dumb(x);

    return x;
}

int
main(int argc,char **argv)
{
    char *cp;
    int lim;
    int *ptr;
    int code;

    --argc;
    ++argv;

    for (;  argc > 0;  --argc, ++argv) {
        cp = *argv;
        if (*cp != '-')
            break;

        switch (cp[1]) {
        case 'S':
            opt_S = cp[2];
            break;
        }
    }

    switch (opt_S) {
    case 'f':  // cause segfault
        ptr = NULL;
        *ptr = 23;
        code = 91;
        break;

    case 'a':  // abort
        abort();
        code = 92;
        break;

    case 't':  // terminate us
        signal(SIGTERM,SIG_DFL);
#if 0
        kill(getpid(),SIGTERM);
#else
        raise(SIGTERM);
#endif
        code = 93;
        break;

    default:
        code = 0;
        break;
    }

    if (argc > 0)
        lim = atoi(argv[0]);
    else
        lim = 10000;

    lim = spin(lim);
    lim &= 0x7F;
    if (code == 0)
        code = lim;

    return code;
}

Вот версия, которая использует ptrace это намного быстрее, чем версия, которая использует gdb:

// ptxctl -- control via ptrace

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
//#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/user.h>

int opt_d;                              // 1=show debug output
int opt_e;                              // 1=echo progress
int opt_f;                              // 1=set line buffered output

unsigned long long sicount;             // single step count

int stopflg;                            // 1=stop seen

pid_t pidtgt;                           // gdb's pid
pid_t pidfin;                           // stop pid

int status;                             // target's final status
char statbuf[1000];                     // status buffer
int coredump;                           // 1=core dumped

int zpxlvl;                             // current trace level

int regsidx;                            // regs index
struct user_regs_struct regs[2];        // current regs

#define REGSALL(_cmd) \
    _cmd(r15) \
    _cmd(r14) \
    _cmd(r13) \
    _cmd(r12) \
    _cmd(rbp) \
    _cmd(rbx) \
    _cmd(r11) \
    _cmd(r10) \
    _cmd(r9) \
    _cmd(r8) \
    _cmd(rax) \
    _cmd(rcx) \
    _cmd(rdx) \
    _cmd(rsi) \
    _cmd(rdi) \
    _cmd(orig_rax) \
    /*_cmd(rip)*/ \
    _cmd(cs) \
    _cmd(eflags) \
    _cmd(rsp) \
    _cmd(ss) \
    _cmd(fs_base) \
    _cmd(gs_base) \
    _cmd(ds) \
    _cmd(es) \
    _cmd(fs) \
    _cmd(gs)

#define REGSDIF(_reg) \
    if (cur->_reg != prev->_reg) \
        printf("  %16.16llX " #_reg "\n",cur->_reg);

double tvelap;                          // start time

#ifndef _USE_ZPRT_
#define _USE_ZPRT_      1
#endif

static inline int
zprtok(int lvl)
{

    return (_USE_ZPRT_ && (opt_d >= lvl));
}

#define dbg(_lvl,_fmt...) \
    do { \
        if (zprtok(_lvl)) \
            printf(_fmt); \
    } while (0)

// tvgetf -- get high precision time
double
tvgetf(void)
{
    struct timespec ts;
    double sec;

    clock_gettime(CLOCK_REALTIME,&ts);
    sec = ts.tv_nsec;
    sec /= 1e9;
    sec += ts.tv_sec;

    return sec;
}

// ptxstatus -- decode status
char *
ptxstatus(int status)
{
    int zflg;
    int signo;
    char *bp;

    bp = statbuf;
    *bp = 0;

    // NOTE: do _not_ use zprtok here -- we need to force this on final
    zflg = (opt_d >= zpxlvl);

    do {
        if (zflg)
            bp += sprintf(bp,"%8.8X",status);

        if (WIFSTOPPED(status)) {
            signo = WSTOPSIG(status);
            if (zflg)
                bp += sprintf(bp," WIFSTOPPED signo=%d",signo);

            switch (signo) {
            case SIGTRAP:
                break;
            default:
                stopflg = 1;
                break;
            }
        }

        if (WIFEXITED(status)) {
            if (zflg)
                bp += sprintf(bp," WIFEXITED code=%d",WEXITSTATUS(status));
            stopflg = 1;
        }

        if (WIFSIGNALED(status)) {
            signo = WTERMSIG(status);
            if (zflg)
                bp += sprintf(bp," WIFSIGNALED signo=%d",signo);

            if (WCOREDUMP(status)) {
                coredump = 1;
                stopflg = 1;
                if (zflg)
                    bp += sprintf(bp," -- core dumped");
            }
        }
    } while (0);

    return statbuf;
}

// ptxcmd -- issue ptrace command
long
ptxcmd(enum __ptrace_request cmd,void *addr,void *data)
{
    long ret;

    dbg(zpxlvl,"ptxcmd: ENTER cmd=%d addr=%p data=%p\n",cmd,addr,data);
    ret = ptrace(cmd,pidtgt,addr,data);
    dbg(zpxlvl,"ptxcmd: EXIT ret=%ld\n",ret);

    return ret;
}

// ptxwait -- wait for target to be stopped
void
ptxwait(const char *reason)
{

    dbg(zpxlvl,"ptxwait: %s pidtgt=%d\n",reason,pidtgt);
    pidfin = waitpid(pidtgt,&status,0);

    // NOTE: we need this to decide on stop status
    ptxstatus(status);

    dbg(zpxlvl,"ptxwait: %s status=(%s) pidfin=%d\n",
        reason,statbuf,pidfin);
}

// ptxwhere -- show where we are
void
ptxwhere(int initflg)
{
    struct user_regs_struct *cur;
    struct user_regs_struct *prev;

    do {
        prev = &regs[regsidx];

        if (initflg) {
            ptxcmd(PTRACE_GETREGS,NULL,prev);
            break;
        }

        regsidx = ! regsidx;
        cur = &regs[regsidx];

        ptxcmd(PTRACE_GETREGS,NULL,cur);
        printf("RIP: %16.16llX (%llu)\n",cur->rip,sicount);

        if (opt_e < 2)
            break;

        REGSALL(REGSDIF);
    } while (0);
}

// ptxctl -- control ptrace
void
ptxctl(int argc,char **argv)
{

    pidtgt = fork();

    // launch target program
    if (pidtgt == 0) {
        pidtgt = getpid();
        ptxcmd(PTRACE_TRACEME,NULL,NULL);
        execvp(argv[0],argv);
        exit(9);
    }

#if 0
    sleep(1);
#endif

    zpxlvl = 1;

#if 0
    ptxwait("SETUP");
#endif

    // attach to tracee
    // NOTE: we do _not_ need to do this because child has done TRACEME
#if 0
    dbg(zpxlvl,"ptxctl: PREATTACH\n");
    ptxcmd(PTRACE_ATTACH,NULL,NULL);
    dbg(zpxlvl,"ptxctl: POSTATTACH\n");
#endif

    // wait for initial stop
#if 1
    ptxwait("INIT");
#endif

    if (opt_e)
        ptxwhere(1);

    dbg(zpxlvl,"ptxctl: START\n");

    tvelap = tvgetf();

    zpxlvl = 2;

    while (1) {
        dbg(zpxlvl,"ptxctl: SINGLESTEP\n");
        ptxcmd(PTRACE_SINGLESTEP,NULL,NULL);
        ptxwait("WAIT");

        sicount += 1;

        // show where we are
        if (opt_e)
            ptxwhere(0);

        dbg(zpxlvl,"ptxctl: STEPCOUNT sicount=%lld\n",sicount);

        // stop when target terminates
        if (stopflg)
            break;
    }

    zpxlvl = 0;
    ptxstatus(status);
    printf("ptxctl: STATUS (%s) pidfin=%d\n",statbuf,pidfin);

    // get elapsed time
    tvelap = tvgetf() - tvelap;
}

// main -- main program
int
main(int argc,char **argv)
{
    char *cp;

    --argc;
    ++argv;

    for (;  argc > 0;  --argc, ++argv) {
        cp = *argv;
        if (*cp != '-')
            break;

        switch (cp[1]) {
        case 'd':
            cp += 2;
            opt_d = (*cp != 0) ? atoi(cp) : 1;
            break;

        case 'e':
            cp += 2;
            opt_e = (*cp != 0) ? atoi(cp) : 1;
            break;

        case 'f':
            cp += 2;
            opt_f = (*cp != 0) ? atoi(cp) : 1;
            break;
        }
    }

    if (argc == 0) {
        printf("specify target program\n");
        exit(1);
    }

    // set output line buffering
    switch (opt_f) {
    case 0:
        break;

    case 1:
        setlinebuf(stdout);
        break;

    default:
        setbuf(stdout,NULL);
        break;
    }

    ptxctl(argc,argv);

    // print statistics
    printf("%llu instructions -- ELAPSED: %.9f -- %.3f insts / sec\n",
        sicount,tvelap,(double) sicount / tvelap);

    return 0;
}

Одним из способов сделать это может быть ручная обработка каждой инструкции инструкцией подсчета. Есть несколько способов сделать это -

  1. Вы можете изменить часть генератора инструкций любого компилятора с открытым исходным кодом (gcc/LLVM), чтобы он выдавал команду подсчета перед каждой инструкцией. Я могу добавить к ответу точный способ сделать это в LLVM, если вам интересно. Но я верю, что второй метод, который я здесь приведу, будет легче реализовать и будет работать с большинством компиляторов.

  2. Вы можете использовать инструкции после компиляции. Большинство компиляторов предоставляют возможность генерировать читаемую сборку вместо объектных файлов. Флаг для GCC / Clang является -S, Для следующей программы

#include <stdio.h>
int main_real(int argc, char* argv[]) {
    printf("hello world\n");
    return 0;
}

мой компилятор выдает следующее .s файл -

    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 14
    .globl  _main_real                  ## -- Begin function main
    .p2align    4, 0x90
_main_real:                                  ## @main_real
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    subq    $32, %rsp
    leaq    L_.str(%rip), %rax
    movl    $0, -4(%rbp)
    movl    %edi, -8(%rbp)
    movq    %rsi, -16(%rbp)
    movq    %rax, %rdi
    movb    $0, %al
    callq   _printf
    xorl    %ecx, %ecx
    movl    %eax, -20(%rbp)         ## 4-byte Spill
    movl    %ecx, %eax
    addq    $32, %rsp
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz  "hello world\n"


.subsections_via_symbols

Здесь легко увидеть, что все начинается с <tab> не сопровождается . это инструкция.

Теперь нам предстоит простая программа, которая находит все такие инструкции и инструктирует их. Вы можете сделать это легко с perl, Но прежде чем мы на самом деле применяем код, мы должны найти подходящую инструкцию. Это будет во многом зависеть от архитектуры и целевой операционной системы. Поэтому я приведу пример для X86_64.

Понятно, почему мы должны инструктировать ПЕРЕД инструкциями, а не ПОСЛЕ их, чтобы также посчитать команды ветвления.

Предполагая глобальные переменные __r13_save а также __instruction_counter инициализируя в ноль, мы можем вставить инструкцию -

movq %r13, __r13_save(%rip)
movq __instruction_counter(%rip), %r13
leaq 1(%r13), %r13
movq %r13, __instruction_counter(%rip)
movq %r13, __r13_save(%rip)

Как вы можете видеть, мы использовали rip режим относительной адресации, который подходит для большинства программ, которые пишет новичок (более крупные программы могут иметь проблемы). Мы использовали leaq здесь вместо incq чтобы избежать засорения флагов, которые используются программой для управления потоком. (Как предложено @PeterCordes в комментариях.)

Этот инструментарий также работает правильно для однопоточных программ, так как мы используем глобальный счетчик для инструкций и скрываем %r13 регистр. Для расширения вышеперечисленного для многопоточной программы нужно будет использовать локальное хранилище потока и также использовать функции создания потока.

Кроме того, переменные __r13_save а также __instruction_counter часто доступны и всегда должны быть в кеше L1, что делает эту аппаратуру не такой дорогой.

Теперь для инструментирования инструкций мы используем Perl как -

cat input.s | perl -pe 's/^(\t[^.])/\tmovq %r13, __r13_save(%rip)\n\tmovq __instruction_counter(%rip), %r13\n\tleaq 1(%r13), %r13\n\tmovq %r13, __instruction_counter(%rip)\n\tmovq %r13, __r13_save(%rip)\n\1/' > output.s

Для приведенного выше примера программы это создает

    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 14
    .globl  _main_real              ## -- Begin function main_real
    .p2align    4, 0x90
_main_real:                             ## @main_real
    .cfi_startproc
## %bb.0:
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    subq    $32, %rsp
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    leaq    L_.str(%rip), %rax
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    movl    %edi, -4(%rbp)
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    movq    %rsi, -16(%rbp)
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    movq    %rax, %rdi
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    movb    $0, %al
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    callq   _printf
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    xorl    %ecx, %ecx
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    movl    %eax, -20(%rbp)         ## 4-byte Spill
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    movl    %ecx, %eax
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    addq    $32, %rsp
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    popq    %rbp
    movq %r13, __r13_save(%rip)
    movq __instruction_counter(%rip), %r13
    leaq 1(%r13), %r13
    movq %r13, __instruction_counter(%rip)
    movq %r13, __r13_save(%rip)
    retq
    .cfi_endproc
                                        ## -- End function
    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz  "hello world\n"


.subsections_via_symbols

Теперь нам также нужно где-то создать эту переменную. Это может быть сделано путем создания простого c wrapper.c как -

#include <stdio.h>
long long int __instruction_counter;
long long int __r13_save;
int main_real(int, char* []);
int main(int argc, char* argv[]) {
    int ret = main_real(argc, argv);
    printf("Total instructions = %lld\n", __instruction_counter);
    return ret;
}

Вы можете увидеть функцию main_real, Таким образом, в вашей реальной программе вы должны создать main_real вместо main,

Наконец, связать все как -

clang output.s wrapper.c -o a.out

и выполнить вашу программу. Ваш код должен работать нормально и печатать счетчик команд до его выхода.

Возможно, вам придется позаботиться о калечении имени __instruction_counter переменная. Для некоторых ABI компилятор добавляет дополнительный _ в начале. В этом случае вам придется добавить дополнительный _ к команде perl. Вы можете проверить точное имя переменной, также сгенерировав сборку для оболочки.

При запуске приведенного выше примера я получаю -

hello world
Total instructions = 15

Что соответствует точному количеству инструкций нашей функции. Возможно, вы заметили, что это учитывает только количество инструкций в коде, который вы написали и скомпилировали. Не в printf функция, например. Как правило, это сложная проблема для статических приборов.

Единственное предостережение в том, что ваша программа должна выйти "нормально", то есть, вернувшись из main, Если это вызывает exit или же abort, вы не сможете увидеть количество команд. Вы также можете предоставить инструментальную версию exit а также abort чтобы решить эту проблему.

С подходом, основанным на компиляторе, это может быть сделано более эффективным путем добавления одного addq инструкция для каждого базового блока с параметром, являющимся номером инструкции, которую имеет BB, поскольку, как только поток управления входит в базовый блок, он обязательно должен пройти его.

Вы можете использовать компилятор Godbolt для компиляции вашей программы и отображения кода сборки для различных компиляторов и опций.

Затем посчитайте количество инструкций для каждого фрагмента, то есть: последовательность операторов до и включая первый тест.

Затем инструмент, который вы кодируете: добавьте глобальную переменную instruction_count, инициализированный к числу инструкций в main Функция epilog и увеличить эту переменную в начале каждого фрагмента на количество инструкций, которые вы посчитали на предыдущем шаге. и напечатайте этот номер непосредственно перед возвращением из main функция.

Вы получите количество инструкций, которые будут выполнены неинструментированной программой для любого ввода данных в программу, для данной комбинации архитектуры, компилятора и опций, но не включая инструкции, выполняемые в функциях библиотеки, ни на этапах запуска, ни на выходе,

Другие вопросы по тегам