qemu는 어떤 명령을 추적합니까?
/ bin / ls 를 단계별로 수행 하고 지침을 계산 하는 다음 코드를 작성했습니다 .
#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <sys/syscall.h>
int main()
{
pid_t child;
child = fork(); //create child
if(child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
char* child_argv[] = {"/bin/ls", NULL};
execv("/bin/ls", child_argv);
}
else {
int status;
long long ins_count = 0;
while(1)
{
//stop tracing if child terminated successfully
wait(&status);
if(WIFEXITED(status))
break;
ins_count++;
ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
}
printf("\n%lld Instructions executed.\n", ins_count);
}
return 0;
}
이 코드를 실행하면 약 500.000 명령이 실행됩니다. 내가 아는 한, 이러한 지침의 대부분은 동적 링커에서 제공되어야합니다. qemu-x86_64 -singlestep -D log -d in_asm / bin / ls 으로 QEMU로 / bin / ls 를 추적 하면 약 17.000 개의 명령이 실행됩니다. QEMU와 동일한 지점에서 계산을 시작하고 중지하려면 무엇을 조정해야합니까? (일명 동일한 지침 계산).
QEMU를 사용하여 "return null"프로그램을 추적했는데 코드가 109025를 제공하는 동안 7840 명령이 발생했습니다. 따라서 QEMU는 주 코드보다 더 많이 추적하지만 내 코드보다 적은 것으로 보입니다.
내 목표는 나중에 이러한 지침을 비교하는 것이므로 QEMU와 같은 동일한 지침을 반복하고 싶습니다.
답변
QEMU의 "in_asm"로깅은 실행 된 명령의 로그가 아닙니다. 명령어가 번역 될 때마다 (즉, QEMU가 이에 상응하는 호스트 코드를 생성 할 때) 기록합니다. 그런 다음 해당 번역이 캐시되고 게스트가 반복해서 동일한 명령을 다시 실행하면 QEMU는 단순히 동일한 번역을 다시 사용하므로 in_asm에 의해 기록되지 않습니다. 따라서 "in_asm에서 훨씬 적은 수의 명령을보고합니다."가 예상됩니다.
-d 옵션을 통해 실행 된 모든 명령어를 로깅하는 것은 약간 까다 롭습니다 .'cpu '및'exec '추적을 살펴보고 -d의'nochain '하위 옵션을 사용하여 그렇지 않으면 QEMU 최적화를 비활성화해야합니다. 일부 블록은 기록되지 않고 '-singlestep'을 사용하여 블록 당 하나의 명령을 강제하고 실행 추적을 인쇄 한 다음 실제로 명령을 실행하지 않는 몇 가지 코너 케이스를 고려합니다. -d 옵션은 사용자가 프로그램의 동작을 검사하는 방법이 아니기 때문입니다. 이것은 QEMU와 게스트 프로그램이 함께 수행하는 작업을 디버깅 할 수 있도록 의도 된 디버그 옵션이므로 다음과 같은 정보를 인쇄합니다. 올바르게 해석하려면 QEMU 내부에 대한 약간의 이해가 필요합니다.
대신 QEMU "플러그인"을 작성하는 것이 더 간단 할 수 있습니다. https://qemu.readthedocs.io/en/latest/devel/tcg-plugins.html-이것은 "카운트 명령어 실행"과 같은 계측을 작성하기 위해 매우 간단하게 설계된 API입니다. 운이 좋으면 샘플 플러그인 중 하나가 목적에 충분할 수도 있습니다.
fork () 앞에 다음 코드를 추가하여 전용 CPU 코어 (예 : 번호 7)에서 실행되도록 프로그램을 수정했습니다 .
#define _GNU_SOURCE
#include <sched.h>
[...]
cpu_set_t set;
int rc;
CPU_ZERO(&set);
CPU_SET(7, &set);
// Migrate the calling process on the target cpu
rc = sched_setaffinity(0, sizeof(cpu_set_t), &set);
if (0 != rc) {
fprintf(stderr, "sched_setaffinity(): '%m' (%d)\n", errno);
return -1;
}
// Dummy system call to trigger the migration. Actually, the on line
// manual says that the previous call will make the current process
// migrate but I saw in cpuid's source code that the guy calls sleep(0)
// to make sure that the migration will be done. In my opinion, it may
// be safer to call sched_yield()
rc = sched_yield();
if (0 != rc) {
fprintf(stderr, "sched_yield(): '%m' (%d)\n", errno);
return -1;
}
// Create child
child = fork();
[...]
내 PC에서 Ubuntu / Linux 5.4.0을 실행하고 있습니다.
# Intel(R) Core(TM) i7-3770K CPU @ 3.50GHz
# Code name : Ivy Bridge
# cpu family : 6
# model : 58
# microcode : 0x21
# Number of physical cores: 4
# Number of harware threads: 8
# Base frequency: 3,50 GHz
# Turbo frequency: 3,90 GHz
# cpu MHz: 1604.615
# cache size : 8192 KB
# cache_alignment: 64
# Address sizes: 36 bits physical, 48 bits virtual
#
# PMU version: 3
# Maximum number of fixed counters: 3
# Fixed counter bit width: 48
# Maximum number of programmable counters: 4
# Programmable counter bit width: 48
ptrace ()가 활성화 된 상태에서 수정 된 프로그램을 시작하면 거의 동일한 숫자를 얻게됩니다.
$ test/progexec
[...]
548765 Instructions executed.
인텔 PMU 카운터를 읽는 도구를 설계했습니다. 고정 카운터 # 0은 다음과 같습니다.
# INST_RETIRED.ANY
#
# Number of instructions that retire execution. For instructions that consist of multiple
# uops, this event counts the retirement of the last uop of the instruction. The counter
# continues counting during hardware interrupts, traps, and in-side interrupt handlers.
#
프로그램이 실행되는 CPU 코어 # 7에서 위의 카운터를 읽으면 다음과 같은 결과가 나타납니다.
- 1871879 사용자 + 커널 공간 실행 지침 (링 0-3)
- 546874 사용자 공간 실행 지침 (링 3)
- 1324451 커널 공간 실행 지침 (링 0)
따라서 위의 숫자에 따라 ptrace (PTRACE_SINGLESTEP) 가있는 프로그램은 프로그램이 사용자 공간 (인텔 보호 링 # 3) 에서 실행 중일 때 명령 수를 계산합니다 .
주의 : Linux는 커널 공간에 링 0을 사용 하고 사용자 공간에 링 3을 사용합니다.