Suche memory-stream zum Erstellen und Modifizieren von Bytecode



  • Hallo,
    wie der Titel schon sagt, benötige ich einen Stream, welcher mir zum einen standard read/write Operationen anbietet, sowie (WICHTIG!) einen direkten Zugriff per Pointer auf die Daten.
    Der Pointer wird vor allem deswegen benötigt, weil ich dieses als eine Art Program-Counter nutze und damit durch den Code navigiere.
    Außerdem ist wichtig, das dieser Pointer auch genau auf die Daten aus dem Stream zeigt und nicht nur eine Kopie.
    Mein bisheriger Ansatz war folgender, welcher aber gerade zuletzt genannten Punkt nicht unterstützt:

    typedef u_char* BytecodePtr;
    
    #define PUT_CMD_METHOD \
    	{ \
    		write((u_char*)(&v), sizeof(v)); \
    	}
    #define SET_CMD_METHOD \
    	{ \
    		u_int last = tellp(); \
    		seekp(addr); \
    		write((u_char*)(&v), sizeof(v)); \
    		seekp(last); \
    	}
    
    class BytecodeWriter: public std::basic_stringstream<u_char> {
    public:
    	BytecodePtr getCodeArray();
    	std::vector<u_char> getCode();
    	std::vector<u_char>* getCodeVector();
    
    	void putChar(const char v) PUT_CMD_METHOD
    	/* SNIP */
    	void putDouble(const double v) PUT_CMD_METHOD
    
    	void setChar(const u_int addr, const char v) SET_CMD_METHOD
    	/* SNIP */
    	void setDouble(const u_int addr, const double v) SET_CMD_METHOD
    };
    
    BytecodePtr BytecodeWriter::getCodeArray() {
    	seekp(0, std::ios::end);
    	u_char* code = new u_char[tellp()];
    	seekg(0, std::ios::beg);
    	read(code, tellp());
    	return code;
    }
    
    std::vector<u_char> BytecodeWriter::getCode() {
    	seekp(0, std::ios::end);
    	std::vector<u_char> code = std::vector<u_char>();
    	code.resize(tellp());
    	seekg(0, std::ios::beg);
    	read(&code.front(), tellp());
    	return code;
    }
    
    std::vector<u_char>* BytecodeWriter::getCodeVector() {
    	seekp(0, std::ios::end);
    	std::vector<u_char>* code = new std::vector<u_char>();
    	code->resize(tellp());
    	seekg(0, std::ios::beg);
    	read(&code->front(), tellp());
    	return code;
    }
    

    Ich schaffe es hier leider nicht, einen direkten Pointer auf den intern vom stringstream -> stringbuffer verwendeten String zu bekommen, um direkten Zugriff auf die Daten zu haben.

    Ich würde mich um Lösungsvorschläge und bessere alternativen sehr freuen.
    Hinweis: Vorzugsweise bitte kein Boost - ich würde gerne bei der STL bleiben.
    PS: Ich bin noch recht neu in C++, kenne mich jedoch in anderen Sprachen besser aus.

    Mit freundlichen Grüßen
    Olee



  • Hallo Olee,

    willkommen im C++-Forum.

    olee schrieb:

    wie der Titel schon sagt, benötige ich einen Stream, welcher mir zum einen standard read/write Operationen anbietet, sowie (WICHTIG!) einen direkten Zugriff per Pointer auf die Daten.
    Der Pointer wird vor allem deswegen benötigt, weil ich dieses als eine Art Program-Counter nutze und damit durch den Code navigiere.

    Das ist ein wenig ein Widerspruch. Ein Stream ist ein 'Strom von Daten', vielleicht vergleichbar einer Wasserleitung - Zugriff hast Du im Allgemeinen nur am Hahn oder an der Einspeisestelle, auch wenn seek und tell was anderes vermuten lassen.
    Ein Pointer im Memory ist ein sogenannter 'random access' - d.h. ein wahlfreier Zugriff. Das ist was anderes.

    Am ehesten passt auf Deine Beschreibung noch ein std::basic_stringbuf<> oder vielleicht ein selbst geschriebener Stream auf Basis eines std::basic_ios<>. Was soll denn z.B. putDouble(const double v) genau tun?

    Gruß
    Werner



  • Was ich meine ist, dass ich gerne einen Stream verwenden würde, um (vorher unbekannt viele) Daten zu schreiben (dafür sind die ganzen put-Befehle, welche nur einfache utility-Funktionen sind für die verschiedenen einfachen Typen).
    Das ist mir auch damit gelungen. Wenn ich den Bytecode generiere, sieht das dann zum Beispiel nur noch so aus (cw ist der BytecodeWriter):

    cw->put(opLod);
    cw->putInt(ref.address + ref.offset);
    cw->putInt(ref.type->getSize());
    

    Doch an anderer Stelle muss ich einen Teil des erstellten Bytecodes nochmal durchgehen und mit Daten ausbessern, welche beim ersten Erstellen nicht vorhanden waren (wie zum Beispiel die Sprungadresse für ein return). Dafür benötige ich aber einen u_char* auf die Daten, da ich so durch den code navigiere (Bytecode-Befehle sind unterschiedlich groß je nach Befehl).

    Meine aktuelle Lösung sieht so aus:

    void Compiler::relocateReturnJumps(int codeStart, int returnAddress) {
    	BytecodeWriter::__string_type data = cw->str();
    	u_char* c = (u_char*)data.c_str() + codeStart;
    	u_char* cend = (u_char*)data.c_str() + cw->tellp();
    	while (c < cend) {
    		if (*c == opJp) {
    			c++;
    			if (*(int*)c == JMP_RETURN)
    				*(int*)c = returnAddress;
    			c += 4;
    		} else
    			c = skipFSSInstruction(c);
    	}
    	//cw->str(data);
    	cw->seekp(0, std::ios::beg);
    	cw->write(data.c_str(), data.length());
    }
    

    Hier wird jedoch extra eine Kopie des Codes angelegt, überarbeitet und anschließend der alte im Stream überschrieben.
    Wie kann ich das sonst realisieren?
    Ich habe früher mit Delphi gearbeitet, wo ich einen TMemoryStream verwendet hätte, welcher eine Property 'Memory' als Pointer zum direkten Zugriff auf die Daten zur Verfügung stellt und auch beim Schreiben von Daten den Speicherbereich vergrößert falls nötig.

    Wie kann ich das in C++ wenn möglich mit der STL ohne groß eigenen Code zu schreiben realisieren?
    Ich benötige im Grunde nur Zugriff auf den internen String im Stringbuffer des Stringstreams, welches aber protected und damit nicht verfügbar ist.

    EDIT: Ich kenne mich leider bisher nur wenig mit der IO-Architektur von C++ aus. Daher frage ich hier ja nach ^^

    MFG Olee



  • Was hält dich davon ab an die passende Stelle zu springen und einfach die Adresse zu überschreiben? setInt müsste das bei dir heißen.
    EDIT: Du müsstest natürlich diese Stellen während der Generierung speichern.

    Warum nimmst du überhaupt einen stringstream ? Das kann man doch ganz leicht mit einem vector<char> selber bauen und hat damit direkten Zugriff.

    Du musst keine Makros nehmen, wo Templates ideal sind.



  • * Verwende Indexe statt Zeiger.
    Zeiger sind in dem Zusammenhang problematisch, da sich die Adresse ändert wenn realloziert wird.
    D.h. im Prinzip nach jedem Anhängen von neuen Befehlen kann das passieren.

    * Das ganze "new" Gedöns in deinem Code sieht schauderhaft aus, lass das lieber.

    * Wozu überhaupt einen Stream? Wieso nicht gleich std::vector<char> ?

    * Wenns unbedingt ein Stream sein muss, dann implementiere lieber deinen eigenen Stream-Buffer. Ist nicht SO schwer wie man meinen könnte, vor allem wenn man nur Output unterstützen muss.

    • tellp() nach seekg(0, std::ios::beg) wird wohl nicht das gewünschte Ergebnis liefern

    * Deine getXxx Funktionen sichern den File-Pointer nicht, d.h. du kannst nach getXxx eh nicht weiter in den Stream schreiben (wenn ich mich nicht irre). => Falls du das nicht brauchst nimm doch einfach nen unmodifizierten std::stringstream , und hol dir den String mit der str() Funktion.

    Ala

    std::string codeStr = stream.str();
    char const* ptr = codeStr.c_str(); // bleibt gültig so lange du "codeStr" in Ruhe lässt
    


  • hustbaer schrieb:

    * Wozu überhaupt einen Stream? Wieso nicht gleich std::vector<char> ?

    Ich wüsste ja gerne wie ich das mit dem mache. Und kann man denn an einen vector einfach so Daten anhängen?

    Der Grund wieso ich einen Stream verwende ist ja, dass ich zu >90% nur Daten anhängen muss. Wie mache ich so etwas mit einem vector? Könnte eine nette Person 😃 vielleicht einen kleinen Code zeigen wie sowas geht?

    Wie ja erwähnt kenne ich mich in dem Bereich bisher noch nicht groß aus.

    hustbaer schrieb:

    • tellp() nach seekg(0, std::ios::beg) wird wohl nicht das gewünschte Ergebnis liefern

    * Deine getXxx Funktionen sichern den File-Pointer nicht, d.h. du kannst nach getXxx eh nicht weiter in den Stream schreiben (wenn ich mich nicht irre). => Falls du das nicht brauchst nimm doch einfach nen unmodifizierten std::stringstream , und hol dir den String mit der str() Funktion.

    Ala

    std::string codeStr = stream.str();
    char const* ptr = codeStr.c_str(); // bleibt gültig so lange du "codeStr" in Ruhe lässt
    

    Die alte Position wird nach Aufruf der get-Funktionen nicht mehr benötigt und deswegen nicht wiederhergestellt.



  • olee schrieb:

    hustbaer schrieb:

    * Wozu überhaupt einen Stream? Wieso nicht gleich std::vector<char> ?

    Ich wüsste ja gerne wie ich das mit dem mache. Und kann man denn an einen vector einfach so Daten anhängen?

    Natürlich kann man das, es gibt sogar mehrere Möglichkeiten: insert , resize , push_back .

    So kann das portabel aussehen:

    #include <vector>
    #include <cassert>
    #include <cstdint>
    
    struct little_endian
    {
    	template <class T>
    	static char get_byte(T value, std::size_t i)
    	{
    		return static_cast<char>(value >> (i * 8));
    	}
    };
    
    struct big_endian
    {
    	template <class T>
    	static char get_byte(T value, std::size_t i)
    	{
    		return static_cast<char>(value >> ((sizeof(value) - i - 1) * 8));
    	}
    };
    
    struct native
    {
    	template <class T>
    	static char get_byte(T value, std::size_t i)
    	{
    		return reinterpret_cast<const char *>(&value)[i];
    	}
    };
    
    template <class Endianness, class T, class Out>
    void write_bytes(Out dest, T value)
    {
    	for (std::size_t i = 0; i < sizeof(value); ++i)
    	{
    		*(dest++) = Endianness::get_byte(value, i);
    	}
    }
    
    template <class Endianness, class T>
    void overwrite_bytes(std::vector<char> &buffer, std::size_t position, T value)
    {
    	auto dest = buffer.begin() + position;
    	write_bytes<Endianness>(dest, value);
    }
    
    template <class Endianness, class T>
    void append_bytes(std::vector<char> &buffer, T value)
    {
    	buffer.resize(buffer.size() + sizeof(value));
    	auto dest = (buffer.end() - sizeof(value));
    	write_bytes<Endianness>(dest, value);
    }
    
    int main()
    {
    	std::vector<char> le, be, na;
    
    	{
    		append_bytes<little_endian, std::uint16_t>(le, 0x1234);
    		assert(le[0] == '\x34');
    		assert(le[1] == '\x12');
    	}
    
    	{
    		const auto position = be.size();
    		append_bytes<big_endian, std::uint16_t>(be, 0xbbaa);
    		assert(be[0] == '\xbb');
    		assert(be[1] == '\xaa');
    
    		overwrite_bytes<big_endian, std::uint16_t>(be, position, 0x1234);
    		assert(be[0] == '\x12');
    		assert(be[1] == '\x34');
    	}
    
    	{
    		append_bytes<native, std::uint16_t>(na, 0x1234);
    		assert(
    			(na == le) !=
    			(na == be)
    			);
    	}
    }
    

    Äquivalent beim Lesen.
    Du musst vorsichtig sein bei Casts von char * zu anderen Typen wie int * . Nicht jeder Rechner kommt damit klar, wenn ein int an einer nicht durch sizeof(int) teilbaren Adresse liegt (Alignment). Deswegen ist so etwas in C++ undefiniertes Verhalten.



  • Ok ich weiß nicht wie mir das entgehen konnte, aber nach den Hinweisen von allen hier hab ich es nochmal richtig mit vector probiert und bin zur Lösung gekommen:

    class BytecodeWriter: public std::vector<u_char> {
    public:
    	BytecodePtr getCodeArray();
    
    	template<class T> void put(const T v) {
    		const size_type address = size();
    		resize(address + sizeof(T));
    		*(T*)(data() + address) = v;
    	}
    
    	template<class T> void set(size_type address, const T v) {
    		*(T*)(data() + address) = v;
    	}
    }
    

    Danke für die Hilfe!



  • olee schrieb:

    bin zur Lösung gekommen

    Das ist nicht "die" Lösung (es gibt sowieso immer mehr als nur eine Lösung). std::vector ist nicht dazu gedacht, um davon abzuleiten. Du hast hier auch keinen Grund dazu, benutze lieber Komposition als Vererbung - das sollte man generell machen, wenn es möglich ist.

    Davon mal ganz abgesehen wirst du in Bytecode selten was anderes als Opcodes (je 1 byte), "integer" und "pointer" schreiben (je 4 oder 8 byte). Double- und Stringliterale usw. landen typischerweise in einem Konstanten-Pool, ähnlich dem Datensegment eines "normalen" Programms. Macht die ganze Verarbeitug auch deutlich einfacher, vor Allem wenn man das Schreiben des eigentlichen Bytecode von einem Bytecodeassembler machen lässt.


Anmelden zum Antworten