Interpreter Brainfucka napisany w C – code review

0

Witam wszystkich

Ostatnio popełniłem taki oto wpis na swoim blogu: https://shizz3r.blogspot.com/2018/08/brainfuck-interpreter-in-c-fifth-day.html#more Post ten jest napisany w ostatni dzień, w którym zakończyłem projekt interpretera opartego o implementację linked listy i stosu.

Oto kod:

#include <stdio.h>
#include <sysexits.h>
#include <string.h>
#include <stdlib.h>

#define EX_SUCCESS 0

#define str1_is_less_than_str2 (strcmp("bf", file_extension) < 0)
#define str2_is_less_than_str1 (strcmp("bf", file_extension) > 0)

#define MEMSIZE 30000
#define NUMBER_OF_BF_INSTRUCTIONS 8

#define FIRST_ELEMENT_NOT_EXISTS (*head_ptr == NULL)

struct bf_instruction_node
{
	int bf_instruction;
	struct bf_instruction_node *next_element;
};

struct stack_node
{
	struct bf_instruction_node *bf_instr_ptr;
	struct stack_node *link;
};

void inc_ptr(int **values_ptr)
{
	++(*values_ptr);
}

void dec_ptr(int **values_ptr)
{
	--(*values_ptr);
}

void check_if_value_is_correct(int *values_ptr)
{
	if( *values_ptr < 0 ) 
	{
		*values_ptr = 255;
	}
	else if( *values_ptr > 255 )
	{
		*values_ptr = 0;
	}
}

void inc_value(int *values_ptr)
{
	++(*values_ptr);
	check_if_value_is_correct(values_ptr);
}

void dec_value(int *values_ptr)
{
	--(*values_ptr);
	check_if_value_is_correct(values_ptr);
}

void print_value(int *values_ptr)
{
	putchar(*values_ptr);
}

void input_value(int *values_ptr)
{
	*values_ptr = getchar();
}

void push(struct stack_node **esp_ptr, struct bf_instruction_node *current_instr_ptr)
{
	struct stack_node *new_element_on_the_stack;
	
	new_element_on_the_stack = (struct stack_node *)malloc(sizeof(struct stack_node));
	new_element_on_the_stack->bf_instr_ptr = current_instr_ptr;
	new_element_on_the_stack->link = *esp_ptr;
	*esp_ptr = new_element_on_the_stack;
}

void pop(struct stack_node **esp_ptr)
{
	struct stack_node *tmp;

	tmp = *esp_ptr;
	*esp_ptr = (*esp_ptr)->link;
	free(tmp);
}

void start_loop(int *values_ptr, struct stack_node **esp_ptr, struct bf_instruction_node **current_instr_ptr)
{
		if( *values_ptr != 0 ) 
		{
			push(esp_ptr, *current_instr_ptr);
		}
		else
		{
			int balance = 0;

			do
			{
				if( (*current_instr_ptr)->bf_instruction == '[' ) 
				{
					balance++;
				}
				else if ( (*current_instr_ptr)->bf_instruction == ']' ) 
				{
					balance--;
				}
				if( balance != 0 ) 
				{
					*current_instr_ptr = (*current_instr_ptr)->next_element;
				}
			} while( balance > 0 );
		}
}

void end_loop(int *values_ptr, struct bf_instruction_node **current_instr_ptr, struct stack_node **esp_ptr)
{
	if( *values_ptr != 0  )
	{
		check_if_value_is_correct(values_ptr);
		*current_instr_ptr = (*esp_ptr)->bf_instr_ptr;
	}
	else
	{
		pop(esp_ptr);
	}
}

void execute_instructions(int **values_ptr, struct bf_instruction_node *head_ptr, struct bf_instruction_node **current_instr_ptr)
{
	struct stack_node *esp_ptr = NULL;

	char brainfuck_instruction;
	*current_instr_ptr = head_ptr;

	while( *current_instr_ptr != NULL )
	{
		brainfuck_instruction = (*current_instr_ptr)->bf_instruction;

		switch( brainfuck_instruction )
		{
			case '>': inc_ptr(values_ptr);									break;
			case '<': dec_ptr(values_ptr);									break;
			case '+': inc_value(*values_ptr);								break;
			case '-': dec_value(*values_ptr);								break;
			case '.': print_value(*values_ptr);								break;
			case ',': input_value(*values_ptr);								break;
			case '[': start_loop(*values_ptr, &esp_ptr, current_instr_ptr);	break;
			case ']': end_loop(*values_ptr, current_instr_ptr, &esp_ptr);	break;
		}
		*current_instr_ptr = (*current_instr_ptr)->next_element;
	}
}

struct bf_instruction_node *create_new_element(struct bf_instruction_node *head_ptr, struct bf_instruction_node **current_instr_ptr, int char_from_file)
{
	struct bf_instruction_node *new_element;

	*current_instr_ptr = head_ptr;
	
	while( (*current_instr_ptr)->next_element != NULL )
		*current_instr_ptr = (*current_instr_ptr)->next_element;

	new_element = (struct bf_instruction_node *)malloc(sizeof(struct bf_instruction_node));

	return new_element;
}

void add_instruction_to_the_list(struct bf_instruction_node **head_ptr, struct bf_instruction_node **current_instr_ptr, int char_from_file)
{
	if( FIRST_ELEMENT_NOT_EXISTS )
	{
		*head_ptr = (struct bf_instruction_node *)malloc(sizeof(struct bf_instruction_node));

		if( *head_ptr == NULL )
		{
			perror("Memory allocation failed");
			exit(EXIT_FAILURE);
		}
		else
		{
			(*head_ptr)->bf_instruction = char_from_file;
			(*head_ptr)->next_element = NULL;
			*current_instr_ptr = *head_ptr;
		}
	}
	else
	{
		struct bf_instruction_node *new_element = create_new_element(*head_ptr, current_instr_ptr, char_from_file);

		if( new_element == NULL )
		{
			perror("Memory allocation failed.");
			exit(EXIT_FAILURE);
		}
		else
		{
			new_element->bf_instruction = char_from_file;
			new_element->next_element = NULL;
			(*current_instr_ptr)->next_element = new_element;
				*current_instr_ptr = new_element;
			}
	}
}

void print_instructions(struct bf_instruction_node *head_ptr, struct bf_instruction_node **current_instr_ptr)
{
	*current_instr_ptr = head_ptr;

	while( *current_instr_ptr != NULL )
	{
		printf("%c", (*current_instr_ptr)->bf_instruction);
		*current_instr_ptr = (*current_instr_ptr)->next_element;
	}
}

void clear_the_memory(struct bf_instruction_node *head_ptr, struct bf_instruction_node **current_instr_ptr)
{
	struct bf_instruction_node *earlier_element;

	*current_instr_ptr = head_ptr;

	while( (*current_instr_ptr) != NULL )
	{
		earlier_element = *current_instr_ptr;
		*current_instr_ptr = (*current_instr_ptr)->next_element;
		free(earlier_element);
	}

	 puts("Memory is cleared.");
}

const char *is_bf_instruction(int char_from_file)
{
	const char bf_alphabet[NUMBER_OF_BF_INSTRUCTIONS] = {'>', '<', '+', '-', ',', '.', '[', ']'};
	return memchr(bf_alphabet, char_from_file, sizeof(bf_alphabet));
}

const char *get_file_extension(const char *filename)
{
	const char *dot = strchr(filename, '.');

	if( dot == NULL )
		return NULL;

	const char *file_extension = dot + 1;
	return file_extension;
}

int main(int argc, char **argv)
{
	if( argc != 2 )
	{
		fprintf(stderr, "File not specified.\n");
		puts("usage: ./bf_interpreter <filename.bf>");
		return EX_USAGE;
	}

	const char *filename = argv[1];
	const char *file_extension = get_file_extension(filename);

	if( file_extension == NULL || str1_is_less_than_str2 || str2_is_less_than_str1 )
	{
		fprintf(stderr, "Incorrect file extension.\n");
		puts("usage: ./bf_interpreter <filename.bf>");
		return EX_DATAERR;
	}

	FILE *file_with_bf_code = fopen(filename, "r");

	if( file_with_bf_code == NULL )
	{
		perror(filename);
		return EX_NOINPUT;
	}

	int values[MEMSIZE] = {0};
	int *values_ptr = values;
	int char_from_file;

	struct bf_instruction_node *head_ptr = NULL, *current_instr_ptr;

	while( (char_from_file = fgetc(file_with_bf_code)) != EOF )
	{
		if( is_bf_instruction(char_from_file) != NULL )
			add_instruction_to_the_list(&head_ptr, &current_instr_ptr, char_from_file);
	}

	execute_instructions(&values_ptr, head_ptr, &current_instr_ptr);
	clear_the_memory(head_ptr, &current_instr_ptr);

	return EX_SUCCESS;
}

Byłbym wdzięczny jeśli ktoś wskazałby mi jakiekolwiek błędy w kodzie. Im więcej wskazanych błędów, tym lepiej! Z góry wszystkim dziękuję

7

Cytując siebię z mikrobloga:

Tak prywatnie, to straszny overengineering w tym projekcie ;). Cały interpreter brainfucka można lajtowo zamknąc w 50-100 linijkach (zaleznie od zwięzłości stylu) a Ty już do 300 dochodzisz. Schludność kodu OK, ale jakość często biedna (np. te definy na górze - O_o).

2. czystość kodu

Przez "schludność" kodu rozumiem np.:

  • opisowe nazwy zmiennych
  • formatowanie kodu i wcięcia
  • dobre używanie whitespace i klamer (brak niepotrzebnych pustych linii, puste linie gdzie dodają czytelności, konsekwentne {})
  • konsekwentne stawianie * przy zmiennych
  • itd - to jest OK.

Prywatnie dużo bardziej wolę if (argc != 2) od if( argc != 2 ), ale tak długo jak konsekwentnie używasz to nie ma znaczenia.
Tylko dwie uwagi w tym punkcie. Pierwsza to długośc linii - czasami do 140 znaków. Polecam się ograniczać do 100 (albo 80, ale w C++ to IMO przesada). Druga to:

            case '>': inc_ptr(values_ptr);                                  break;
            case '<': dec_ptr(values_ptr);                                  break;
            case '+': inc_value(*values_ptr);                               break;
            case '-': dec_value(*values_ptr);                               break;
            case '.': print_value(*values_ptr);                             break;
            case ',': input_value(*values_ptr);                             break;
            case '[': start_loop(*values_ptr, &esp_ptr, current_instr_ptr); break;
            case ']': end_loop(*values_ptr, current_instr_ptr, &esp_ptr);   break;

IMO alignowanie breaków (albo czegokolwiek innego) w kodzie to zło. Zazwyczaj problem jest tylko estetyczny (a każdy lubi co innego), ale w tym przypadku ma znaczenie praktyczne - jesli ktoś zmieni case ']' na coś odrobinę dłuższego, to wszystkie break się przesuną w prawo. Nie dość że to dodatkowa, niepotrzebna praca, to jeszcze psuje to później wynik narzędzi typu git blame (używasz gita, prawda :P).

2. implementacja

Tu trochę gorzej. Do overengineeringu dojdziemy, ale chodzi mi o rzeczy typu:

#define str1_is_less_than_str2 (strcmp("bf", file_extension) < 0)
#define str2_is_less_than_str1 (strcmp("bf", file_extension) > 0)

...

int main()
{
    const char *file_extension = get_file_extension(filename);
 
    if( file_extension == NULL || str1_is_less_than_str2 || str2_is_less_than_str1 )
    {
        fprintf(stderr, "Incorrect file extension.\n");
        // ...
    }
}

O_o Dennis Ritchie się w grobie przewraca.

  1. Skoro file_extension istnieje tylko w mainie to używanie go w define to zbrodnia wojenna. Jak juz to funkcja do tego
  2. W dodatku, w tym przypadku, tych definów używasz tylko raz, więc czemu ich po prostu nie inlinować ręcznie?
  3. Wtedy byś zauważył od razu że sprawdzasz czy (strcmp < 0) || (strcmp > 0) i zamieniłbyś całość na (strcmp != 0).
  4. Ostatni krok do oświecenia to (IMO) zauważenie że jesli ktoś chce zapisac kod brainfucka do pliku test.txt zamiast test.bf, to nie ma po co mu tego zabraniać. Poleganie na rozszerzeniu pliku, poza - może - Windowsem, to antypattern ;). O ile nie ma dobrego powodu żeby zrobić inaczej, cały if do wyrzucenia.

To samo odnośnie:

#define FIRST_ELEMENT_NOT_EXISTS (*head_ptr == NULL)

Czemu w ogóle używać makr do tego? :P

Trochę inny problem z makrem:

#define NUMBER_OF_BF_INSTRUCTIONS 8

...

    const char bf_alphabet[NUMBER_OF_BF_INSTRUCTIONS] = {'>', '<', '+', '-', ',', '.', '[', ']'};

Głupio tak zapisywać jedną informację w dwóch miejscach. No i ta stała nie jest nigdzie używana. Wniosek - usunąć ją i zamienić kod na:

    const char bf_alphabet[] = {'>', '<', '+', '-', ',', '.', '[', ']'};

A następnie na (praktycznie równoważnie)

    const char bf_alphabet[] = "><+-,.[]";

Inna sprawa:

    while( (char_from_file = fgetc(file_with_bf_code)) != EOF )

czytanie po bajcie jest woolne (nie sprawdzałem, ale to pewnie robi syscall znak po znaku). Nie ma pewnie znaczenia tutaj, ale to bardzo zły nawyk.

Dalej:

    int values[MEMSIZE] = {0};

Trochę duża tablica żeby ją trzymać na stosie - na niektórych systemach nie masz takiego dużego stosu, nie mówiąc o tym że to średnia praktyka. W tym przypadku lepiej zrobić malloc.

3. złożoność

Wydaje mi się że to samo co masz w tym kodzie da się uzyskać prościej. Rzędu jakoś 3x prościej. Przykładowo implementacja BF na szybko (naklepana na kolanie w celu demonstracji, nie bić):

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

char *source =
    "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<++++++"
    "+++++++++.>.+++.------.--------.>+.>.";

const int BF_INVALID_LOOP = 1;

struct bf_state {
  int head;
  size_t memory_size;
  uint8_t *memory;
};

struct bf_state bf_init(int memory_size) {
  struct bf_state result = {
    0, memory_size, malloc(memory_size)
  };
  return result;
}

int bf_move(int direction, char open, char close, int *index, char *source) {
  assert(direction == 1 || direction == -1);
  assert(source != NULL && index != NULL && source[*index] == open);

  int balance = 0;
  while (index > 0 && source[*index] != '\0') {
    balance += (source[*index] == open);
    balance -= (source[*index] == close);
    if (balance == 0) {
      return 0;
    }
    *index += direction;
  }
  return 1;
}

int bf_run(struct bf_state *state, char *source) {
  assert(state != NULL && source != NULL);

  int source_ptr = 0;
  while (source[source_ptr] != '\0') {
    switch (source[source_ptr]) {
      case '+': state->memory[state->head]++; break;
      case '-': state->memory[state->head]--; break;
      case '>': state->head = (state->head + 1) % state->memory_size; break;
      case '<': state->head = (state->head - 1) % state->memory_size; break;
      case '.': putchar(state->memory[state->head]); break;
      case ',': state->memory[state->head] = getchar(); break;
      case '[':
        if (state->memory[state->head] == 0) {
          if (bf_move(1, '[', ']', &source_ptr, source)) {
            return BF_INVALID_LOOP;
          }
        }
        break;
      case ']':
        if (state->memory[state->head] != 0) {
          if (bf_move(-1, ']', '[', &source_ptr, source)) {
            return BF_INVALID_LOOP;
          }
        }
        break;
      default: break;  
    }
    source_ptr++;
  }
  return 0;
}

int main() {
  int memory_size = 1000;
  struct bf_state machine = bf_init(memory_size);
  int error = bf_run(&machine, source);
  if (error != 0) {
    printf("Interpreter error code: %d\n", error);
  }
  return error;
}

Gdyby to było pisane w bardziej przyjaznym obiektowosci języku, zamiast bf_state i metod pewnie by powstał obiekt. Ale jest w C, więc...

Dwie rzeczy jakich brakuje w stosunku do Twojego kodu to:

  • czytanie z pliku (to akurat proste: policzenie wielkości pliku jakimś ftellem, malloc i fread)
  • trzymanie stosu pętli w celu szybszej interpretacji (generalnie być może premature optimization - wrócimy do tego).

Za to co jest:

  • dużo przyjaźniejsze i elastyczniejsze API. Np. zamiast robić pętle po napisach, wystarczy przekazać char* ze źródłem. Kontrolowalna wielość bufora pamięci.
  • faktycznie w miarę szczelne API - przy błedzie użytkownika zwróci błąd (BF_INVALID_LOOP) zamiast wołać exit(). Dzięki temu można by tego kodu użyć w większym projekcie jako modułu/biblioteki. Albo napisać sensowne testy. Za to błedy programisty są sprawdzane za pomocą assert (podział jest dość płynny, asserty użyte trochę dla wygody - można by też zwracać kody błędów, bo wyjątków w C nie ma).
  • bezpieczne (...a przynajmniej chyba). Na oko przy obsłudze <> nie sprawdzasz w ogóle wychodzenia poza krawędzie. To znaczy, że gdybyś np. wystawił ten program użytkownikom w internecie, to ktoś odpowiednio zinterpretowany mógłby wyjśc za tablicę, podmienić odpowiednie wartości w pamięci, napisać exploita i "zhackować" Ci serwer. Średnio. Albo, mniej groźnie, po prostu rzucić segfault.
  • nie marnuje pamięci - po co trzymać bajty jako int? ;) BF był zaprojektowany w czasach kiedy wszyscy pisali niskopoziomowo - to nie przypadek że komórki idealnie pasują do bajta. Używając uint8_t (aka unsigned char w praktyce) zużywasz 4x albo 8x mniej pamięci i nie musisz się martwić obsługą overflowów.
  • unika skomplikowanego żonglowania pointerami i mallocami - Twój kod aż prosi się o bug/segfault. Fakt ze linkujesz do blog posta gdzie go debugujesz printami o tym tez świadczy ;)

A co mogłoby być:
Mając dobre API do kodu można go potem łatwo rozwijać w różne strony. Na przykład:

  • parametr mówiący ile maksymalnie instrukcji wykonać (ważne w praktyce, żeby zapobiec nieskończonym pętlom w interpreterze)
  • wsparcie do breakpointów i/lub single-steppowania, żeby umożliwić debugowanie kodu.
  • wsparcie dla jakiegoś dialektu brainfucka, który obsługuje więcej operacji niż 8.
  • optymalizacja pętli (skoki w O(1) zamisat O(n))
  • kompilacja JIT ;)

Ważne żeby wiedziec co sie chce osiągnąć, bo zaimplementowanie tych wszystkich "poprawek" doprowadzi do stworzenia potwora. Więc trzeba pomyśleć jak wyobrażasz sobie efekt końcowy i do niego dążyć. Może na pewon słaba wydajność to nie problem? Albo nie interesuje Cię debugowanie.

Jedyne co na pewno IMO można poprawić to usunięcie ukrytej zależności od globalnego stanu. Otóż aktualnie kod czyta z stdin i pisze na stdout. Wszystko fajnie, ale znacznie to utrudnia testowanie albo inne używanie jako biblioteka. Niestety w C nie ma sensownej abstrakcji na to, więc najlepsza opcja IMO tutaj to przekazywanie interpreterowi plików z których ma czytać i do którego ma pisać.

Ogólnie nie jest źle, ale da się lepiej :P.

0
msm napisał(a):

A co mogłoby być:

  • kompilacja JIT ;)

To wcale nie jest takie szalone. Napisałem kiedyś interpreter Brainfucka który generował kod asemblera x86, a potem odpalał na tym (bodajże) YASMa.
Efekt był bardzo wydajny, mimo że chyba jedyną optymalizacją była zamiana wielokrotnych + i - na pojedyncze add i sub.

0

Dziękuję bardzo @msm za tak obszerny post. Doceniam taką pomoc szczególnie, ponieważ mało komu chciałoby się aż tyle pisać, żeby kogoś nakierować na lepsze rozwiązanie danego problemu. Ale mam kilka uwag nie tyle do wartości merytorycznej Twojego postu, ale do funkcjonalności, które zaproponowałeś.

Za to co jest:

  • dużo przyjaźniejsze i elastyczniejsze API. Np. zamiast robić pętle po napisach, wystarczy przekazać char* ze źródłem. Kontrolowalna wielość bufora pamięci.

Odwołując się do tego to zdawałem sobie sprawę, że można spokojnie napisać interpreter Brainfucka opierając się na tablicach typu char wskutek czego rzeczywiście byłaby kontrolowana wielkość bufora pamięci. Powiem więcej - nawet zacząłem implementować takie rozwiązanie, ale natknąłem się na pojęcie linked listy i pomyślałem, że fajnie byłoby się nauczyć dynamicznego alokowania pamięci w C już przy pierwszym projekcie pisanym w tym języku. Dlatego porzuciłem pomysł z tablicami, który wydawał mi się zbyt prosty.

  • unika skomplikowanego żonglowania pointerami i mallocami - Twój kod aż prosi się o bug/segfault. Fakt ze linkujesz do blog posta gdzie go debugujesz printami o tym tez świadczy ;)

To niestety było konieczne zważywszy na to jak zaplanowałem implementację swojego projektu. W początkowych postach na temat interpretera pisałem o tym, że jest bardzo duże ryzyko, że pojawią się SEGFAULTY. Ale pisząc ten projekt właśnie w sposób żonglowania pointerami i mallocami bardzo dużo się nauczyłem i czuję większą swobodą w operacjach na pamięci.

A co mogłoby być:

  • optymalizacja pętli (skoki w O(1) zamisat O(n))

Hmm... to jest ciekawa sprawa. Nie mam na razie zupełnie pomysłu jak miałoby to wyglądać, ale rzeczywiście chciałbym się dowiedzieć.

Co do reszty wytkniętych błędów to pełna zgoda. :) Postaram się je poprawić.

1

Dlatego porzuciłem pomysł z tablicami, który wydawał mi się zbyt prosty.

To niestety było konieczne zważywszy na to jak zaplanowałem implementację swojego projektu. W początkowych postach na temat interpretera pisałem o tym, że jest bardzo duże ryzyko, że pojawią się SEGFAULTY. Ale pisząc ten projekt właśnie w sposób żonglowania pointerami i mallocami bardzo dużo się nauczyłem i czuję większą swobodą w operacjach na pamięci.

Jasne, to wynika z użycia list. Zazwyczaj lepiej napisać coś w prostszy sposób niż skomplikowany, dlatego sugerowałem tablice ;). Ale jeśli to było w celu nauki to jasne, można też tak.

Powiem więcej - nawet zacząłem implementować takie rozwiązanie, ale natknąłem się na pojęcie linked listy i pomyślałem, że fajnie byłoby się nauczyć dynamicznego alokowania pamięci w C już przy pierwszym projekcie pisanym w tym języku

Zacząłbym od tego czego w szkole nie uczą a na uniwersytecie rzadko: linked listy to zło (rzadko) konieczne. Sytuacje kiedy użycie linked listy to dobre rozwiązanie są bardzo rzadkie. A kiedy już jesteś w bardzo nietypowej sytuacji i lista jest dobrym rozwiązanem, najczęściej jest to intruzywna linked lista która jest potwarzą dla wszystkich reguł czystego kodu (ale potrafi zdziałać cuda).

Anyway, spoiler, Twój use case nie jest dobrym miejscem na linked listę ;). Zastosowania wyglądają jak coś co (dynamiczna) tablica by była w stanie zrobić szybciej i prościej.

Btw. trochę argumentów na to co pisałęm:
http://cglab.ca/~abeinges/blah/too-many-lists/book/#an-obligatory-public-service-announcement (blog o ruście, ale wpis trafny dla każdego języka)
https://kjellkod.wordpress.com/2012/02/25/why-you-should-never-ever-ever-use-linked-list-in-your-code-again/ (długi post i nie czytałem :P. Ale wygląda sensownie).

Hmm... to jest ciekawa sprawa. Nie mam na razie zupełnie pomysłu jak miałoby to wyglądać, ale rzeczywiście chciałbym się dowiedzieć.

W najprostszej wersji, zrobić preprocessing i dla każdego [ i ] w źródle trzymać adres odpowiadającego mu ] i [. Wtedy nie trzeba za każdym razem szukać końca pętli, bo i tak za każdym razem jest w tym samym miejscu.

0

Szczerze powiedziawszy po dostosowaniu się do rad @msm ten kod wygląda zupełnie inaczej i widocznie lepiej. Implementacje stosu i linked listy dużo mnie nauczyły, ale nauczyłem się też, że linked listy nie są dobrym rozwiązaniem, a ponadto stos może być tutaj rzeczywiście nazwany jako premature optimization, bo jest zbędny po prostu. Jeśli ktoś z początkujących chciałby się poduczyć działania na wskaźnikach i pamięci to nadal polecam implementować różne struktury danych w swoich projektach - potem i tak kod można zrefaktoryzować. :)

Napisałem taki oto kod (na razie bez dodawania polecanych zmian - to po prostu czysty interpreter):

#include <stdio.h>
#include <stdlib.h>
#include <sysexits.h>
#include <stdint.h>
#include <assert.h>

#define BF_INVALID_LOOP -1

struct bf_state
{
	int values_ptr;
	size_t memory_size;	 
	uint8_t *memory_segment; 
};

size_t get_size_of_a_file(FILE *file_with_bf_code)
{
	fseek(file_with_bf_code, 0, SEEK_END);
	size_t size_of_a_file = ftell(file_with_bf_code);	
	return size_of_a_file;
}

struct bf_state bf_init(size_t memory_size)
{
	struct bf_state result = { 
		0, memory_size, (uint8_t *)malloc(memory_size)
	};
	return result;
}

int jump(char *source, int *source_ptr, int direction)
{
	assert(direction == 1 || direction == -1);
	assert(source != NULL && source_ptr != NULL && (source[*source_ptr] == '[' || source[*source_ptr] == ']'));
	
	int balance = 0;
	do
	{
		if (source[*source_ptr] == '[')
			balance++;
		else if (source[*source_ptr] == ']')
			balance--;

		if (balance == 0)
			return 1;

		*source_ptr += direction;

	}while (*source_ptr > 0 && source[*source_ptr] != '\0');
	return -1;
}

int execute_instructions(struct bf_state *state, char *source)
{
	assert(state != NULL && source != NULL);
	int source_ptr = 0, move_left = (-1), move_right = 1;

	while (source[source_ptr] != '\0')
	{
		switch (source[source_ptr])
		{
			case '>': state->values_ptr = (state->values_ptr + 1) % (state->memory_size); break;
			case '<': state->values_ptr = (state->values_ptr - 1) % (state->memory_size); break;
			case '+': state->memory_segment[state->values_ptr]++; break;
			case '-': state->memory_segment[state->values_ptr]--; break;
			case '.': putchar(state->memory_segment[state->values_ptr]); break;
			case ',': state->memory_segment[state->values_ptr] = getchar(); break;
			case '[': 
				if (state->memory_segment[state->values_ptr] == 0)
				{
					if (!jump(source, &source_ptr, move_right))
					{
						fprintf(stderr, "Invalid loop error.\n");
						return BF_INVALID_LOOP;
					}
				}
				break;
			case ']': 
				if (state->memory_segment[state->values_ptr] != 0)
				{
					if (!jump(source, &source_ptr, move_left))
					{
						fprintf(stderr, "Invalid loop error.\n");
						return BF_INVALID_LOOP;
					}
				}
				break;
			default: break;
		}
		source_ptr++;		
	}
	return EX_OK;
}

int main(int argc, char **argv)
{
	if (argc != 2)
	{
		fprintf(stderr, "File not specified.\n");
		puts("usage: ./bf_interpreter <filename.bf>");
		return EX_USAGE;
	}

	const char *filename = argv[1];
	FILE *file_with_bf_code = fopen(filename, "r");

	if (file_with_bf_code == NULL)
	{
		perror(filename);
		return EX_NOINPUT;
	}

	const int memory_size = 30000;

	size_t size_of_a_file = get_size_of_a_file(file_with_bf_code);
	fseek(file_with_bf_code, 0, SEEK_SET);
	
	char *source = (char *)malloc(size_of_a_file);
	fread(source, 1, size_of_a_file, file_with_bf_code);
	fclose(file_with_bf_code);

	struct bf_state values = bf_init(memory_size);
	int error = execute_instructions(&values, source, maximum_number_of_instructions);

	if (error != 0)
		printf("Interpreter error code: %d\n", error);
	
	free(source);
	return error;
}

Kod jest widocznie krótszy, a działa poprawnie także jest zmiana. Można jeszcze coś w nim poprawić czy mogę się już zabierać za implementację dodatkowych funkcjonalności?

4
  1. Przesadzasz IMO z długością nazw identyfikatorów:
    a) size_t get_size_of_a_file(FILE *file_with_bf_code) -> size_t get_file_size(FILE *file),
    b) size_of_a_file -> file_size.

  2. bf_state.values_ptr brzmi, jak gdyby był wskaźnikiem (_ptr), lecz w rzeczywistości nim nie jest.

  3. Nie potrzebujesz wcale zmiennej lokalnej size_of_a_file wewnątrz get_size_of_a_file - podobnie ze zmienną lokalną wewnątrz bf_init.

  4. assert(source != NULL && source_ptr != NULL && (source[*source_ptr] == '[' || source[*source_ptr] == ']')); - ta linijka jest zbyt długa; rozbij ją na trzy osobne asercje.

  5. Pamiętaj o formatowaniu kodu - w jednym miejscu masz do i dopiero w następnej linii {, a chwilę później }while (bez spacji między klamerką a słowem kluczowym).

  6. Zawartość case '[': oraz case ']': to kopiuj-wklejka.

  7. default: break; - co Ci daje ta instrukcja?

0
int error = execute_instructions(&values, source, maximum_number_of_instructions);

Jeden argument za dużo.

move_left = (-1)

Lol

2

Ten switch wyglądałby lepiej gdybyś napisał zamiast:

            case '>': state->values_ptr = (state->values_ptr + 1) % (state->memory_size); break;
            case '<': state->values_ptr = (state->values_ptr - 1) % (state->memory_size); break;

kod:

            case '>': inc_value_pos(); break;
            case '<': dec_value_pos(); break;
2

Zostałem bezpośrednio zpingowany żeby odpisać po poprawkach (thx), więc odpisuję. Najpierw odniosę się do @Patryk27:

Wszystkie uwagi bardzo dobre, ale do ostatnich dwóch:

  1. Zawartość case '[': oraz case ']': to kopiuj-wklejka.

Z tą zawartością case '[' i case ']' to w sumie tak jak w moim. Ciężko to przepisać sensownie w C, bo:

  • jeden if ma != a drugi ==
  • jeden jump skacze w prawo a drugi w lewo.

Jedyne co faktycznie się powtarza to obsługa błędu, ale klasyczne rozwiązanie tutaj to by było goto a dla zasady nie chciałem go polecać jako czysty kod :P.
Na pewno da się to wyciągnąć jakoś, pytanie czy warto.

default: break; - co Ci daje ta instrukcja?

Defaulty w switchach to trochę kwestia dyskusyjna. Popatrzyłem do góry i faktycznie też mam. Przyzwyczajenie z języków które wymagają kompletności w switchach ;).
Dodatkowo czasami dobrze być explicit i jawnie w kodzie intencje prezentować - w tym przypadku że znaki inne niż +-[]<>., są poprawne ale ignorowane celowo.

Plus wymaganie kompletnych switchy daje parę dodatkowych opcji ze strony kompilatora (bardziej przydatne przy enumach):

    -Wswitch
    Warn whenever a switch statement has an index of enumerated type and lacks a case for one or more of the named codes of that enumeration. (The presence of a default label prevents this warning.) case labels outside the enumeration range also provoke warnings when this option is used. This warning is enabled by -Wall.

    -Wswitch-default
    Warn whenever a switch statement does not have a default case.

    -Wswitch-enum
    Warn whenever a switch statement has an index of enumerated type and lacks a case for one or more of the named codes of that enumeration. case labels outside the enumeration range also provoke warnings when this option is used. 

Więc nie powiedziałbym że to obiektywnie bład (kwestia stylu, ważne żeby sie trzymać jednego).

Z moich uwag:

Prawie wszędzie masz dobrą obsługę wszystkich błędów, więc wyróżnia się:

    char *source = (char *)malloc(size_of_a_file);
    fread(source, 1, size_of_a_file, file_with_bf_code);

Jako że to w main() to łatwo obsłużyć przypadek kiedy zwróci null.

 if (!jump(source, &source_ptr, move_right))

Skoro jump() zawsze zwraca tylko wartości niezerowe (1 dla ok, -1 jako błąd) to nigdy sie to nie wykona.
Przy takiej obsłudze błędów, zwrócenie 0 powinno być dla błędów a dowolna wartośc niezerowa oznacza bład.

Btw. to jest coś co testy automatyczne by trywialnie wyłapały :P.

0

Po kolejnych wprowadzonych zmianach kod wygląda tak:

#include <stdio.h>
#include <stdlib.h>
#include <sysexits.h>
#include <stdint.h>
#include <assert.h>

#define BF_VALID_LOOP 0
#define BF_INVALID_LOOP 1

struct bf_state
{
	int index;
	size_t memory_size;	 
	uint8_t *memory_segment; 
};

size_t get_file_size(FILE *file_with_bf_code)
{
	fseek(file_with_bf_code, 0, SEEK_END);
	return ftell(file_with_bf_code);
}

struct bf_state bf_init(size_t memory_size)
{
	struct bf_state result = { 
		0, memory_size, (uint8_t *)malloc(memory_size)
	};
	return result;
}

void inc_offset(struct bf_state *state)
{
	state->index = (state->index + 1) % (state->memory_size);
}

void dec_offset(struct bf_state *state)
{
	state->index = (state->index - 1) % (state->memory_size);
}

void inc_value(struct bf_state *state)
{
	state->memory_segment[state->index]++;
}

void dec_value(struct bf_state *state)
{
	state->memory_segment[state->index]--;
}

int jump(char *source, int *source_ptr, int direction)
{
	assert(direction == 1 || direction == -1);
	assert(source != NULL);
	assert(source_ptr != NULL);
	assert(source[*source_ptr] != '[' || source[*source_ptr] != ']');
	
	int balance = 0;
	do
	{
		if (source[*source_ptr] == '[')
			balance++;
		else if (source[*source_ptr] == ']')
			balance--;

		if (balance == 0)
			return BF_VALID_LOOP;

		*source_ptr += direction;

	} 
	while (*source_ptr > 0 && source[*source_ptr] != '\0');
	
	return BF_INVALID_LOOP;
}

int execute_instructions(struct bf_state *state, char *source)
{
	assert(state != NULL && source != NULL);
	int source_ptr = 0, move_left = -1, move_right = 1;

	while (source[source_ptr] != '\0')
	{
		switch (source[source_ptr])
		{
			case '>': inc_offset(state); break;
			case '<': dec_offset(state); break;
			case '+': inc_value(state); break;
			case '-': dec_value(state); break;
			case '.': putchar(state->memory_segment[state->index]); break;
			case ',': state->memory_segment[state->index] = getchar(); break;
			case '[': 
				if (state->memory_segment[state->index] == 0)
				{
					int loop_error = jump(source, &source_ptr, move_right);
					if (loop_error)
					{
						fprintf(stderr, "Invalid loop error.\n");
						return BF_INVALID_LOOP;
					}
				}
				break;
			case ']': 
				if (state->memory_segment[state->index] != 0)
				{
					int loop_error = jump(source, &source_ptr, move_left);
					if (loop_error)
					{
						fprintf(stderr, "Invalid loop error.\n");
						return BF_INVALID_LOOP;
					}
				}
				break;
		}
		source_ptr++;		
	}
	return EX_OK;
}

int main(int argc, char **argv)
{
	if (argc != 2)
	{
		fprintf(stderr, "File not specified.\n");
		puts("usage: ./bf_interpreter <filename.bf>");
		return EX_USAGE;
	}

	const char *filename = argv[1];
	FILE *file_with_bf_code = fopen(filename, "r");

	if (file_with_bf_code == NULL)
	{
		perror(filename);
		return EX_NOINPUT;
	}

	const int memory_size = 30000;

	size_t file_size = get_file_size(file_with_bf_code);
	fseek(file_with_bf_code, 0, SEEK_SET);
	
	char *source = (char *)malloc(file_size);
	assert(source != NULL);

	size_t read_elements = fread(source, 1, file_size, file_with_bf_code);

	if (!read_elements)
	{
		fprintf(stderr, "Brainfuck code not found.\n");
		fclose(file_with_bf_code);
		return EX_DATAERR;
	}

	fclose(file_with_bf_code);

	struct bf_state values = bf_init(memory_size);
	int error = execute_instructions(&values, source);

	if (error != 0)
		printf("Interpreter error code: %d\n", error);
	
	free(source);
	return error;
}

Niektóre fragmenty kodu poprawiłem wedle własnego uznania.

  1. Dodałem na przykład zmienną loop_error, która według mnie dodaje czytelności, bo napisanie if (jump(...)) jest złym pomysłem, ponieważ to wyrażenie będzie prawdziwe gdy wystąpi błąd. Dla mnie jest to mało czytelne, dużo bardziej wolę wersję ze zmienną.
  2. Kolejna sprawa to ten default w switchu. Osobiście bardziej podoba mi się wersja bez tej instrukcji dlatego jej nie zastosowałem.
  3. Postanowiłem też zostawić zmienną lokalną w funkcji bf_init. Jeśli ja czytałbym ten kod wolałbym, żeby ta zmienna jednak była zwracana - jakoś schludniej to wygląda, choć może się mylę.
  4. Tak jak wcześniej pisałem - wydaję mi się, że tak kopiuj-wklejka nie jest tak do końca niepoprawna. Po pierwsze to właśnie nie jest to kopiuj-wklej słowo w słowo, a po drugie ciężko było mi wymyślić inny sposób na rozwiązanie tego przypadku (przyznam, że po prostu tego sposobu nie wymyśliłem)

Jak ten kod teraz wygląda? Może taki zostać?

1
msm napisał(a):

Z tą zawartością case '[' i case ']' to w sumie tak jak w moim. Ciężko to przepisać sensownie w C, bo:

  • jeden if ma != a drugi ==
  • jeden jump skacze w prawo a drugi w lewo.

] można też interpretować jako bezwarunkowy skok do odpowiedniego [ (ale nie za, tylko do).

0

Chciałbym odkopać ten temat, ponieważ po skończeniu większego projektu w Pythonie naszła mnie ochota, aby zaimplementować kilka funkcji do interpretera w ramach nauki C. Napotkałem jednak pewien problem i niestety mam całkowitą pustkę w głowie i nie wiem zbytnio jak ruszyć z miejsca. Otóż chciałbym prosić o pomoc w kwestii zaprojektowania kodu obsługującego pętle zagnieżdżone. Ten kod musi być jednak kodem maszynowym, ponieważ chodzi o stworzenie JIT'a. Na ten moment stworzyłem coś takiego:

#include <stdio.h>
#include <stdlib.h>
#include <sysexits.h>
#include <stdint.h>
#include <assert.h>
#include <string.h>
#include <sys/mman.h>
#include "vector.h"

#define BF_VALID_LOOP 0
#define BF_INVALID_LOOP 1

struct bf_state
{
	int index;
	int source_ptr;
	uint8_t *memory_segment;
	size_t memory_size;
};

size_t get_file_size(FILE *file_with_bf_code)
{
	fseek(file_with_bf_code, 0, SEEK_END);
	return ftell(file_with_bf_code);
}

struct bf_state bf_init(size_t memory_size)
{
	struct bf_state result = { 
		0, 0, (uint8_t *)malloc(memory_size), memory_size
	};
	return result;
}
/*
int jump(char *source, struct bf_state *state, int direction)
{
	assert(direction == 1 || direction == -1);
	assert(source != NULL);
	assert(source[state->source_ptr] != '[' || source[state->source_ptr] != ']');
	
	int balance = 0;
	do
	{
		if (source[state->source_ptr] == '[')
			balance++;
		else if (source[state->source_ptr] == ']')
			balance--;

		if (balance == 0)
			return BF_VALID_LOOP;

		state->source_ptr += direction;

	} 
	while (state->source_ptr > 0 && source[state->source_ptr] != '\0');
	
	return BF_INVALID_LOOP;
}*/

void jit(struct bf_state *state, char *source)
{
	assert(state != NULL && source != NULL);
	int move_left = -1, move_right = 1;
	struct vec vector;
	
	vector_alloc(&vector);

	char jit_prologue[] = {
	    0x55,	// push rbp
		0x48, 0x89, 0xe5,	// mov rbp, rsp
		0x50,	// push rax
		0x51,	// push rcx
		0x52,	// push rdx
		0x48, 0x89, 0xf8,	// mov rax, rdi
		0x48, 0x8b, 0x48, 0x08,	// mov rcx, [rax+8]
		0x48, 0x8b, 0x50, 0x04	// mov rdx, [rax+4]
	};
	push_back(&vector, jit_prologue, sizeof(jit_prologue));

	while (source[state->source_ptr] != '\0')
	{
		switch (source[state->source_ptr])
		{
			case '>': 
			{
			    char jit_inc_ptr[] = {
			        0x48, 0xff, 0xc1	// inc rcx
				}; 
				push_back(&vector, jit_inc_ptr, sizeof(jit_inc_ptr));
		    }
			break;
			case '<':
			{
			    char jit_dec_ptr[] = {
				   0x48, 0xff, 0xc9	// dec rcx
				};
				push_back(&vector, jit_dec_ptr, sizeof(jit_dec_ptr));
			}
			break;
			case '+':
			{
			    char jit_inc_value[] = {
				   0xfe, 0x01
				}; // inc byte [rcx]
				push_back(&vector, jit_inc_value, sizeof(jit_inc_value));
			}
			break;
			case '-':
			{
			    char jit_dec_value[] = {
				    0xfe, 0x09 // dec byte [rcx]
				};
				push_back(&vector, jit_dec_value, sizeof(jit_dec_value));
			}
			break;  
			case '.':
			{
			    char jit_putchar[] = {
				    // using linux syscall write
				    0x50, // push rax
					0x51, // push rcx
					0x52, // push rdx
					0xba, 0x01, 0x00, 0x00, 0x00, // mov rdx, 1
					0x48, 0x89, 0xc9, // mov rcx, rcx
					0xbb, 0x01, 0x00, 0x00, 0x00, // mov rbx, 1
					0xb8, 0x04, 0x00, 0x00, 0x00, // mov rax, 4
					0xcd, 0x80, // int 0x80
					0x5a, // pop rdx
					0x59, // pop rcx
					0x58 // pop rax 
				};
				push_back(&vector, jit_putchar, sizeof(jit_putchar));
			}
			break;
			case ',':
			{
			    char jit_getchar[] = {
				    0x50, // push rax
					0x51, // push rcx
					0x51, // push rdx
					0xba, 0x01, 0x00, 0x00, 0x00, // mov edx, 1
					0x48, 0x89, 0xc9, // mov rcx, rcx
					0xbb, 0x00, 0x00, 0x00, 0x00, // mov ebx, 0
					0xb8, 0x03, 0x00, 0x00, 0x00, // mov eax, 4
					0xcd, 0x80, // int 0x80
					0x5a, // pop rdx
					0x59, // pop rcx
					0x58 // pop rax
				};
				push_back(&vector, jit_getchar, sizeof(jit_getchar));
			}
			break; 
			case '[': 
				if (state->memory_segment[state->index] == 0)
				{
					int loop_error = jump(source, state, move_right);
					if (loop_error)
					{
						fprintf(stderr, "Invalid loop error.\n");
						// return BF_INVALID_LOOP;
					}
				}
				break;
			case ']': 
				if (state->memory_segment[state->index] != 0)
				{
					int loop_error = jump(source, state, move_left);
					if (loop_error)
					{
						fprintf(stderr, "Invalid loop error.\n");
						// return BF_INVALID_LOOP;
					}
				}
				break;
		}
		
		state->source_ptr++;
		//printf("%c", source[state->source_ptr]); 
} 

    char jit_epilogue[] = {
	    0x5a,	// pop rdx
		0x59,	// pop rcx
		0x58,	// pop rax
		0x5d,	// pop rbp
		0xc3	// ret
	};
	push_back(&vector, jit_epilogue, sizeof(jit_epilogue));

	void *jit_mem = mmap(NULL, vector.elements_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	memcpy(jit_mem, vector.data, vector.elements_size);
	void (*execute_func)(struct bf_state *) = jit_mem;

	execute_func(state);
	munmap(jit_mem, vector.elements_size);
	vector_delete(&vector);
}

int get_number_of_instructions(char *source)
{
	const char bf_alphabet[] = "><+-,.[]";
	int number_of_bf_instr = 0;
	
	for (unsigned int i = 0; i < strlen(source); i++)
	{
		char *is_bf_instruction = memchr(bf_alphabet, source[i], sizeof(bf_alphabet));
		if (is_bf_instruction != NULL)
			number_of_bf_instr++;
	}

	return number_of_bf_instr;
}

int main(int argc, char **argv)
{
	if (argc != 2)
	{
		fprintf(stderr, "File not specified.\n");
		puts("usage: ./bf_interpreter <filename.bf>");
		return EX_USAGE;
	}

	const char *filename = argv[1];
	FILE *file_with_bf_code = fopen(filename, "r");

	if (file_with_bf_code == NULL)
	{
		perror(filename);
		return EX_NOINPUT;
	}

	const int memory_size = 30000;

	size_t file_size = get_file_size(file_with_bf_code);
	fseek(file_with_bf_code, 0, SEEK_SET);
	
	char *source = (char *)malloc(file_size);
	assert(source != NULL);

	size_t read_elements = fread(source, 1, file_size, file_with_bf_code);
	if (!read_elements)
	{
		fprintf(stderr, "Brainfuck code not found.\n");
		fclose(file_with_bf_code);
		return EX_DATAERR;
	}
	
	int number_of_bf_instructions = get_number_of_instructions(source);
	if (number_of_bf_instructions > memory_size)
	{
		fprintf(stderr, "You cannot read more than %d instructions.", memory_size);
		fclose(file_with_bf_code);
		return EX_DATAERR;
	}
	
	fclose(file_with_bf_code);

	struct bf_state values = bf_init(memory_size);
	jit(&values, source);
	free(source);
	
	return 0;
}

Dodatkowo plik z implementacją vector wygląda tak:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct vec
{
    unsigned int elements_size;
	unsigned int capacity;
    char *data;
};

int vector_alloc(struct vec *vector)
{
    vector->elements_size = 0;
    vector->capacity = 100;
    vector->data = (char *)malloc(vector->capacity * sizeof(char));

	if (vector->data == NULL)
	    return -1;
	
	return 0;
}

int vector_delete(struct vec *vector)
{
    if (vector == NULL || vector->data == NULL)
	    return -1;

    vector->elements_size = 0;
	vector->capacity = 0;
	free(vector->data);

	return 0;
}

int push_back(struct vec *vector, char *bytes, int size_of_bytes)
{
    if (vector->elements_size + size_of_bytes > vector->capacity) {
        vector->capacity *= 2;
        vector->data = (char *)realloc(vector->data, vector->capacity * sizeof(char));
		if (vector->data == NULL) {
		   return -1;
		}
    }
   
    memcpy(vector->data + vector->elements_size, bytes, size_of_bytes);
	vector->elements_size += size_of_bytes;
	
	return 0;
}

Chciałbym prosić o pomoc w kwestii właśnie zaimplementowania tych zagnieżdżonych pętli. Oczywiście przeszukałem internet odnośnie tego problemu, ale na razie nie zrozumiałem do końca żadnej z implementacji. Myślałem o tym, żeby te pętle zaimplementować tak samo jak kody w języku C objęte w komentarze tylko przy użyciu Assembly, ale coś mi mówi, że można to zrobić lepiej. Czy ktoś ma jakieś pomysły? Byłbym wdzięczny za każdą merytoryczną pomoc. :)

2

Pomyślałbym o zautomatyzowanej translacji ASM -> char[], bo to jit_prologue wygląda teraz słabo.
Tak się robiło na ZX Spectrum, ale dzisiaj chyba są na to jakieś sposoby.

2

Przepisałeś w asemblerze operacje we/wy, co nic ci w zasadzie nie daje, a skoki nadal widzę że są interpretowane.. to raczej powinno być odwrotnie: pisanie i czytanie znaków możesz zrobić jako call do odpowiedniej funkcji w C, a skoki powinny być zamienione na jmp.
Tylko pojawi ci się konieczność wyliczania offsetów. Dlatego łatwiejsze jest generowanie kodu asm a nie binarnego, i odpalenie na tym zewnętrznego asemblera...

0
vpiotr napisał(a):

Pomyślałbym o zautomatyzowanej translacji ASM -> char[], bo to jit_prologue wygląda teraz słabo.
Tak się robiło na ZX Spectrum, ale dzisiaj chyba są na to jakieś sposoby.

Azarien napisał(a):

Przepisałeś w asemblerze operacje we/wy, co nic ci w zasadzie nie daje, a skoki nadal widzę że są interpretowane.. to raczej powinno być odwrotnie: pisanie i czytanie znaków możesz zrobić jako call do odpowiedniej funkcji w C, a skoki powinny być zamienione na jmp.
Tylko pojawi ci się konieczność wyliczania offsetów. Dlatego łatwiejsze jest generowanie kodu asm a nie binarnego, i odpalenie na tym zewnętrznego asemblera...

Dziękuję Wam za odpowiedzi, ale mam pytanie - jakby ta translacja z Asm do kodu binarnego miałaby wyglądać? Dlaczego reprezentacja kodu Asm w kodzie binarnym od razu miałaby być gorsza niż sam kod Asma? Z tego co czytałem to JITa pisze się właśnie w ten sposób, że wrzuca się do pamięci wykonywalnej kod maszynowy wygenerowany przez Assembler. Moglibyście mi pomóc to zrozumieć?

1
Shizzer napisał(a):
vpiotr napisał(a):

Pomyślałbym o zautomatyzowanej translacji ASM -> char[], bo to jit_prologue wygląda teraz słabo.
Tak się robiło na ZX Spectrum, ale dzisiaj chyba są na to jakieś sposoby.

Azarien napisał(a):

Przepisałeś w asemblerze operacje we/wy, co nic ci w zasadzie nie daje, a skoki nadal widzę że są interpretowane.. to raczej powinno być odwrotnie: pisanie i czytanie znaków możesz zrobić jako call do odpowiedniej funkcji w C, a skoki powinny być zamienione na jmp.
Tylko pojawi ci się konieczność wyliczania offsetów. Dlatego łatwiejsze jest generowanie kodu asm a nie binarnego, i odpalenie na tym zewnętrznego asemblera...

Dziękuję Wam za odpowiedzi, ale mam pytanie - jakby ta translacja z Asm do kodu binarnego miałaby wyglądać? Dlaczego reprezentacja kodu Asm w kodzie binarnym od razu miałaby być gorsza niż sam kod Asma? Z tego co czytałem to JITa pisze się właśnie w ten sposób, że wrzuca się do pamięci wykonywalnej kod maszynowy wygenerowany przez Assembler. Moglibyście mi pomóc to zrozumieć?

Chodzi głównie o maintenance. Co jeśli tych wstawek będzie 100 i trzeba będzie w każdej coś dodać?
Będziesz po kolei kompilował każdą wstawkę i potem ręcznie je wklejał z pliku HEX do C plus formatowanie?

0 użytkowników online, w tym zalogowanych: 0, gości: 0