호그와트

GPT4가 분석한 memcpy

영웅*^%&$ 2023. 6. 8. 15:41
728x90

// compiled with : gcc -o memcpy memcpy.c -m32 -lm
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/mman.h>
#include <math.h>

unsigned long long rdtsc(){
        asm("rdtsc");
}

char* slow_memcpy(char* dest, const char* src, size_t len){
int i;
for (i=0; i<len; i++) {
dest[i] = src[i];
}
return dest;
}

char* fast_memcpy(char* dest, const char* src, size_t len){
size_t i;
// 64-byte block fast copy
if(len >= 64){
i = len / 64;
len &= (64-1);
while(i-- > 0){
__asm__ __volatile__ (
"movdqa (%0), %%xmm0\n"
"movdqa 16(%0), %%xmm1\n"
"movdqa 32(%0), %%xmm2\n"
"movdqa 48(%0), %%xmm3\n"
"movntps %%xmm0, (%1)\n"
"movntps %%xmm1, 16(%1)\n"
"movntps %%xmm2, 32(%1)\n"
"movntps %%xmm3, 48(%1)\n"
::"r"(src),"r"(dest):"memory");
dest += 64;
src += 64;
}
}

// byte-to-byte slow copy
if(len) slow_memcpy(dest, src, len);
return dest;
}

int main(void){

setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stdin, 0, _IOLBF, 0);

printf("Hey, I have a boring assignment for CS class.. :(\n");
printf("The assignment is simple.\n");

printf("-----------------------------------------------------\n");
printf("- What is the best implementation of memcpy?        -\n");
printf("- 1. implement your own slow/fast version of memcpy -\n");
printf("- 2. compare them with various size of data         -\n");
printf("- 3. conclude your experiment and submit report     -\n");
printf("-----------------------------------------------------\n");

printf("This time, just help me out with my experiment and get flag\n");
printf("No fancy hacking, I promise :D\n");

unsigned long long t1, t2;
int e;
char* src;
char* dest;
unsigned int low, high;
unsigned int size;
// allocate memory
char* cache1 = mmap(0, 0x4000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
char* cache2 = mmap(0, 0x4000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
src = mmap(0, 0x2000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

size_t sizes[10];
int i=0;

// setup experiment parameters
for(e=4; e<14; e++){ // 2^13 = 8K
low = pow(2,e-1);
high = pow(2,e);
printf("specify the memcpy amount between %d ~ %d : ", low, high);
scanf("%d", &size);
if( size < low || size > high ){
printf("don't mess with the experiment.\n");
exit(0);
}
sizes[i++] = size;
}

sleep(1);
printf("ok, lets run the experiment with your configuration\n");
sleep(1);

// run experiment
for(i=0; i<10; i++){
size = sizes[i];
printf("experiment %d : memcpy with buffer size %d\n", i+1, size);
dest = malloc( size );

memcpy(cache1, cache2, 0x4000); // to eliminate cache effect
t1 = rdtsc();
slow_memcpy(dest, src, size); // byte-to-byte memcpy
t2 = rdtsc();
printf("ellapsed CPU cycles for slow_memcpy : %llu\n", t2-t1);

memcpy(cache1, cache2, 0x4000); // to eliminate cache effect
t1 = rdtsc();
fast_memcpy(dest, src, size); // block-to-block memcpy
t2 = rdtsc();
printf("ellapsed CPU cycles for fast_memcpy : %llu\n", t2-t1);
printf("\n");
}

printf("thanks for helping my experiment!\n");
printf("flag : ----- erased in this source code -----\n");
return 0;
}


이 프로그램은 고속 메모리 복사 함수(fast_memcpy)와 느린 메모리 복사 함수(slow_memcpy)의 성능을 비교하는 실험을 수행합니다. 각 함수의 성능은 해당 함수를 실행하는데 필요한 CPU 사이클 수로 측정됩니다. 실험은 서로 다른 크기의 메모리 블록을 복사하면서 수행됩니다.

코드를 세부적으로 보면 다음과 같이 나눌 수 있습니다.

rdtsc 함수: 이 함수는 어셈블리 명령 'rdtsc'를 호출하여 현재의 타임스탬프 카운터 값을 반환합니다. 이 값을 이용하여 함수의 실행 시간을 CPU 사이클 단위로 측정합니다.

slow_memcpy 함수: 이 함수는 단순하게 한 바이트씩 복사하는 기본적인 메모리 복사 함수입니다.

fast_memcpy 함수: 이 함수는 'SSE2' 명령어를 이용하여 64 바이트 단위로 메모리를 복사하려고 시도합니다. 만약 복사해야 할 데이터가 64 바이트보다 작으면, slow_memcpy 함수를 이용하여 나머지 부분을 복사합니다.

main 함수: 실험 설정과 실행을 수행하는 주요 함수입니다. 이 함수는 메모리를 할당하고, 실험에 필요한 크기의 데이터를 입력받은 후, 각 크기에 대해 두 가지 복사 함수를 실행하고 실행 시간을 측정하여 출력합니다. 실험은 캐시의 효과를 제거하기 위해 각 복사 사이클 전에 동일한 크기의 캐시를 복사하는 작업을 포함합니다.

실험을 시작하기 전에, 프로그램은 사용자로부터 복사할 데이터 크기를 10번 입력받습니다. 각 입력은 특정 범위에 속해야 합니다. 이 범위는 2^(n-1)에서 2^n 사이여야 합니다, 여기서 n은 4부터 13까지의 숫자입니다. 이 범위는 각 시행마다 증가합니다.

실험은 각각의 크기에 대해 slow_memcpy 함수와 fast_memcpy 함수를 실행하는 10번의 시행으로 구성됩니다. 각 시행마다, 두 함수의 실행 시간은 rdtsc 함수를 사용하여 측정되고, 결과는 표준 출력에 출력됩니다.

실험을 마치면, 프로그램은 사용자에게 감사의 메시지를 출력하고 종료됩니다.

이 코드에서 "flag"라는 단어가 나타나는 유일한 위치는 마지막 출력 메시지입니다: "flag : ----- erased in this source code -----". 그러나 이 메시지는 단순히 텍스트로, 그 자체로 플래그를 제공하지 않습니다.

이 코드에는 플래그를 얻거나 생성하는 로직이 포함되어 있지 않습니다. 따라서 현재 이 코드를 컴파일하고 실행하면 "flag"를 얻을 수 있는 특별한 조건이 없습니다. 주석에서 언급된 "flag"는 아마도 원래의 전체 코드 또는 환경에서 제공되었을 것으로 보입니다.

마지막 printf() 함수를 실행하기 위해서는 이전에 주어진 메모리 복사 연산에 대한 입력이 올바르게 주어져야 합니다.

이 코드의 핵심 부분은 이런 부분입니다:

c
Copy code
for(e=4; e<14; e++){ // 2^13 = 8K
    low = pow(2,e-1);
    high = pow(2,e);
    printf("specify the memcpy amount between %d ~ %d : ", low, high);
    scanf("%d", &size);
    if( size < low || size > high ){
        printf("don't mess with the experiment.\n");
        exit(0);
    }
    sizes[i++] = size;
}
위의 반복문은 사용자에게 10번의 입력을 요구하며, 각 입력은 특정 범위 내에 있어야 합니다. 그 범위는 각 반복에서 다르며, 2^(e-1)과 2^e 사이입니다. 이 때 e의 값은 4에서 13까지 변합니다.

따라서 마지막 printf() 함수를 실행하려면, 이 입력들이 모두 올바르게 주어져야 합니다. 그러기 위해서는, 각 입력이 요구하는 범위 내에 있는 숫자를 입력해야 합니다.

movdqa와 movntps는 두 가지 특수한 복사 연산을 수행하는 x86 어셈블리 명령어입니다.

movdqa: "Move Aligned Double Quadword"의 약자로, 이 명령어는 128비트 데이터를 한 번에 xmm 레지스터로 이동하거나, 반대로 xmm 레지스터의 데이터를 메모리로 전송하는 처리를 합니다. 즉, 크기가 16바이트(128비트)인 블록을 한 번에 복사합니다. 이 명령어는 원본과 목적지가 16바이트 경계에 맞춰진 경우에만 사용할 수 있습니다. 그렇지 않으면 예외가 발생합니다. 이것은 코드에서 "트릭"의 일부가 될 수 있습니다.

movntps: "Move Non-Temporal Packed Single-Precision Floating-Point Values"의 약자로, 이 명령어는 XMM 레지스터의 데이터를 캐시를 거치지 않고 직접 메모리로 이동시킵니다. 이는 일반적으로 대량의 데이터를 복사할 때 사용됩니다. 캐시를 통하지 않고 직접 메모리에 쓰는 것은 프로세서 캐시의 공간을 보존하고 캐시 스래싱을 방지합니다.

8: 0 * 16 + 8
16: 1 * 16
32: 2 * 16
72: 4 * 16 + 8
136: 8 * 16 + 8
264: 16 * 16 + 8
520: 32 * 16 + 8
1032: 64 * 16 + 8
2056: 128 * 16 + 8
4096: 256 * 16
이렇게, 각 범위의 최대값이 16바이트 블록 + 8바이트인 입력값을 선택함으로써, fast_memcpy() 함수가 메모리 블록을 중복으로 복사하려는 문제를 해결할 수 있습니다. 이렇게 하면 마지막 printf() 함수까지 실행될 수 있어 flag를 얻을 수 있게 됩니다.

728x90