Как определить количество машинных инструкций 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 = ®s[regsidx];
if (initflg) {
ptxcmd(PTRACE_GETREGS,NULL,prev);
break;
}
regsidx = ! regsidx;
cur = ®s[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;
}
Одним из способов сделать это может быть ручная обработка каждой инструкции инструкцией подсчета. Есть несколько способов сделать это -
Вы можете изменить часть генератора инструкций любого компилятора с открытым исходным кодом (gcc/LLVM), чтобы он выдавал команду подсчета перед каждой инструкцией. Я могу добавить к ответу точный способ сделать это в LLVM, если вам интересно. Но я верю, что второй метод, который я здесь приведу, будет легче реализовать и будет работать с большинством компиляторов.
Вы можете использовать инструкции после компиляции. Большинство компиляторов предоставляют возможность генерировать читаемую сборку вместо объектных файлов. Флаг для 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
функция.
Вы получите количество инструкций, которые будут выполнены неинструментированной программой для любого ввода данных в программу, для данной комбинации архитектуры, компилятора и опций, но не включая инструкции, выполняемые в функциях библиотеки, ни на этапах запуска, ни на выходе,