diff --git a/.editorconfig b/.editorconfig index 7fb8a25e..1bb53e8f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,8 +8,5 @@ indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true -[*.lua] -indent_size = 2 - [{Makefile,*.mk}] indent_style = unset diff --git a/.gitignore b/.gitignore index cf70b6a8..14448df0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ tests/c/doctest.h .vscode/ # Autogenerated files: -csrc/*_const.cpp +src/unicorn/*_const.lua csrc/cpp_test csrc/basic_control_functions.cpp csrc/registers.cpp diff --git a/Makefile b/Makefile index cc38c357..5ca041e5 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,8 @@ # fail. .SUFFIXES: +.DELETE_ON_ERROR: + ################################################################################ # DEFAULTS # These are only used when this Makefile is run manually. You should only be @@ -59,7 +61,8 @@ ARCHITECTURE_HEADERS = $(wildcard $(UNICORN_INCDIR)/unicorn/*.h) ARCHITECTURE_SLUGS = $(filter-out platform,$(basename $(notdir $(ARCHITECTURE_HEADERS)))) SOURCE_DIR = csrc -CONSTANT_FILES = $(foreach s,$(ARCHITECTURE_SLUGS),$(SOURCE_DIR)/$(s)_const.cpp) +LUA_SOURCE_DIR = src/unicorn +CONSTANT_FILES = $(foreach s,$(ARCHITECTURE_SLUGS),src/unicorn/$(s)_const.lua) CPP_TEMPLATE_SOURCES = $(wildcard $(SOURCE_DIR)/*.template) AUTOGENERATED_CPP_FILES = $(CPP_TEMPLATE_SOURCES:.template=.cpp) @@ -68,7 +71,7 @@ HEADER_TEMPLATE_SOURCES = $(wildcard $(SOURCE_DIR)/unicornlua/*.template) AUTOGENERATED_HPP_FILES = $(HEADER_TEMPLATE_SOURCES:.template=.hpp) LIB_BUILD_TARGET = $(SOURCE_DIR)/unicorn.$(LIB_EXTENSION) -LIB_CPP_SOURCES = $(wildcard $(SOURCE_DIR)/*.cpp) $(CONSTANT_FILES) $(AUTOGENERATED_CPP_FILES) +LIB_CPP_SOURCES = $(wildcard $(SOURCE_DIR)/*.cpp) $(AUTOGENERATED_CPP_FILES) LIB_OBJECT_FILES = $(LIB_CPP_SOURCES:.cpp=.$(OBJ_EXTENSION)) TEST_EXECUTABLE = $(SOURCE_DIR)/cpp_test @@ -148,18 +151,14 @@ export DYLD_FALLBACK_LIBRARY_PATH := $(LIBRARY_COLON_PATHS):$(DYLD_FALLBACK_LIBR # This must be the first rule, don't move it. -$(LIB_BUILD_TARGET): $(LIB_OBJECT_FILES) - $(LINK_CMD) $(LIBFLAG) -o $@ $^ $(REQUIRED_LIBS_FLAGS) +$(LIB_BUILD_TARGET): $(LIB_OBJECT_FILES) $(CONSTANT_FILES) + $(LINK_CMD) $(LIBFLAG) -o $@ $(filter-out %.lua,$^) $(REQUIRED_LIBS_FLAGS) $(TEST_EXECUTABLE): $(TEST_CPP_OBJECT_FILES) $(LIB_OBJECT_FILES) $(LINK_CMD) -o $@ $^ $(REQUIRED_LIBS_FLAGS) $(LINK_TO_LUA_FLAG) -lm -$(SOURCE_DIR)/%_const.cpp: $(UNICORN_INCDIR)/unicorn/%.h - $(LUA) tools/generate_constants.lua $< $@ - - tests/c/%.$(OBJ_EXTENSION): tests/c/%.cpp $(AUTOGENERATED_HPP_FILES) $(TEST_HEADERS) | $(DOCTEST_HEADER) $(CXX_CMD) $(CXXFLAGS) -c -o $@ $< @@ -176,6 +175,18 @@ tests/c/%.$(OBJ_EXTENSION): tests/c/%.cpp $(AUTOGENERATED_HPP_FILES) $(TEST_HEAD $(LUA) tools/render_template.lua -o $@ $^ +$(LUA_SOURCE_DIR)/%_const_gen.c: $(UNICORN_INCDIR)/unicorn/%.h tools/generate_constants.lua + $(LUA) tools/generate_constants.lua $< $@ + + +$(LUA_SOURCE_DIR)/%_const_gen: $(LUA_SOURCE_DIR)/%_const_gen.c + $(CC) $(INCLUDE_PATH_FLAGS) -o $@ $< + + +$(LUA_SOURCE_DIR)/%_const.lua: $(LUA_SOURCE_DIR)/%_const_gen + $< > $@ + + $(DOCTEST_HEADER): $(CURL) -sSo $@ https://raw.githubusercontent.com/doctest/doctest/$(DOCTEST_TAG)/doctest/doctest.h diff --git a/src/unicorn/context.lua b/src/unicorn/context.lua new file mode 100644 index 00000000..5c8bcd36 --- /dev/null +++ b/src/unicorn/context.lua @@ -0,0 +1,58 @@ +--- @module context + +local uc_c = require("unicorn_c_") + +local ContextMethods = {} +local ContextMeta = {__index = ContextMethods} + +function Context(engine, context_handle) + if context_handle == nil then + context_handle = uc_c.context_save(engine) + end + + -- We want to hold a weak reference to the engine so that this context laying around + -- won't prevent it from being collected, but we do need to hold a strong reference to + -- the handle returned to us by Unicorn. Thus, we need to put the engine into a weak + -- table instead of directly in the Context object. + local instance = { + engine_ref_ = setmetatable({engine = engine}, {__mode = "v"}), + handle_ = context_handle, + } + + return setmetatable(instance, ContextMeta) +end + + +function ContextMeta:__close() + if self.handle_ ~= nil then + self:free() + end +end + + +function ContextMeta:__gc() + if self.handle_ ~= nil then + self:free() + end +end + + +function ContextMethods:free() + if self.handle_ == nil then + error("Attempted to free the same context twice.") + end + + -- The engine reference can be nil in two cases: 1) the engine was collected, or 2) + -- this is a double free. We need to check for a double free first, as that's a user + -- bug. This is more serious. + if self.engine_ref_.engine == nil then + error("BUG: Engine was garbage collected before a context.") + end + + uc_c.context_free(self.engine_ref_.engine, self.handle_) + self.engine_ref_.engine = nil + self.handle_ = nil +end + + +return {Context = Context} diff --git a/src/unicorn/engine.lua b/src/unicorn/engine.lua new file mode 100644 index 00000000..9781acab --- /dev/null +++ b/src/unicorn/engine.lua @@ -0,0 +1,229 @@ +--- @module engine + +local uc_c = require("unicorn_c_") +local uc_context = require("unicorn.context") + +local EngineMethods = {} +local EngineMeta = {__index = EngineMethods} + + +--- Create an object-oriented wrapper around an opened Unicorn engine. +--- +--- @param handle A handle returned by the Unicorn C library (not the lua binding). +function Engine(handle) + local instance = { + engine_handle_ = handle, + -- Once a context object is unreachable, it can't be used to restore the engine to + -- the state the context describes. Since there's no point to holding onto a + -- context the user can no longer use, we use a weak table to store them to allow + -- them to be garbage collected once the user can't use them anymore. + -- + -- We still need this table because if there are active contexts laying around + -- when the engine is closed, we need to release those as well. + contexts_ = setmetatable({}, {__mode = "k"}), + hooks_ = {}, + } + + return setmetatable(instance, EngineMeta) +end + +function EngineMeta:__close() + -- Only close the engine if it hasn't been closed already. We want to allow double- + -- closing here because the user may want to explicitly close an engine on some + -- control paths, but let Lua automatically close it on others. + if self.engine_handle_ ~= nil then + self:close() + end +end + +function EngineMeta:__gc() + if self.engine_handle_ ~= nil then + self:close() + end +end + +--- Stop the emulator engine and free all resources. +--- +--- This removes all hooks, frees contexts and memory, then closes the underlying Unicorn +--- engine. The object must not be used after this is called. +function EngineMethods:close() + if self.engine_handle_ == nil then + error("Attempted to close an engine twice.") + end + + self:emu_stop() + uc_c.close(self.engine_handle_) + + -- We need to delete the handle so that when the garbage collector runs, we don't try + -- closing an already deallocated engine. + self.engine_handle_ = nil +end + +function EngineMethods:context_restore(context) + return uc_c.context_restore(self.engine_handle_, context.context_handle_) +end + +function EngineMethods:context_save(context) + local context_handle = uc_c.context_save(self.engine_handle_, context) + return uc_context.Context(self.engine_handle_, context_handle) +end + +function EngineMethods:emu_start(start_addr, end_addr, timeout, n_instructions) + return uc_c.emu_start( + self.engine_handle_, + start_addr, + end_addr, + timeout or 0, + n_instructions or 0 + ) +end + +function EngineMethods:emu_stop() + for context in pairs(self.contexts_) do + context:free() + end + self.contexts_ = {} + + for hook_handle in pairs(self.hooks_) do + hook_handle:close() + end + self.hooks_ = {} + + uc_c.emu_stop(self.engine_handle_) +end + +function EngineMethods:errno() + return uc_c.errno(self.engine_handle_) +end + +function EngineMethods:hook_add() + error("Not implemented yet") +end + +function EngineMethods:hook_del() + error("Not implemented yet") +end + +function EngineMethods:mem_map() + error("Not implemented yet") +end + +function EngineMethods:mem_protect() + error("Not implemented yet") +end + +function EngineMethods:mem_read() + error("Not implemented yet") +end + +function EngineMethods:mem_regions() + error("Not implemented yet") +end + +function EngineMethods:mem_unmap() + error("Not implemented yet") +end + +function EngineMethods:mem_write() + error("Not implemented yet") +end + +function EngineMethods:query(query_flag) + return uc_c.query(self.engine_handle_, query_flag) +end + +function EngineMethods:reg_read() + error("Not implemented yet") +end + +function EngineMethods:reg_read_as() + error("Not implemented yet") +end + +function EngineMethods:reg_read_batch() + error("Not implemented yet") +end + +function EngineMethods:reg_read_batch_as() + error("Not implemented yet") +end + +function EngineMethods:reg_write() + error("Not implemented yet") +end + +function EngineMethods:reg_write_as() + error("Not implemented yet") +end + +function EngineMethods:reg_write_batch() + error("Not implemented yet") +end + + +-- These functions are only available in Unicorn 2.x. +if uc_c.version()[1] > 1 then + function EngineMethods:ctl_exits_disable() + return uc_c.ctl_exits_disable(self.engine_handle_) + end + + function EngineMethods:ctl_exits_enable() + return uc_c.ctl_exits_enable(self.engine_handle_) + end + + function EngineMethods:ctl_flush_tlb() + return uc_c.ctl_flush_tlb(self.engine_handle_) + end + + function EngineMethods:ctl_get_arch() + return uc_c.ctl_get_arch(self.engine_handle_) + end + + function EngineMethods:ctl_get_cpu_model() + return uc_c.ctl_get_cpu_model(self.engine_handle_) + end + + function EngineMethods:ctl_get_exits() + error("Not implemented yet") + end + + function EngineMethods:ctl_get_exits_cnt() + return uc_c.ctl_get_exits_cnt(self.engine_handle_) + end + + function EngineMethods:ctl_get_mode() + return uc_c.ctl_get_mode(self.engine_handle_) + end + + function EngineMethods:ctl_get_page_size() + error("Not implemented yet") + end + + function EngineMethods:ctl_get_timeout() + return uc_c.ctl_get_timeout(self.engine_handle_) + end + + function EngineMethods:ctl_remove_cache() + error("Not implemented yet") + end + + function EngineMethods:ctl_request_cache() + error("Not implemented yet") + end + + function EngineMethods:ctl_set_cpu_model() + error("Not implemented yet") + end + + function EngineMethods:ctl_set_exits() + error("Not implemented yet") + end + + function EngineMethods:ctl_set_page_size() + error("Not implemented yet") + end +end + + +--- @export +return {Engine = Engine} diff --git a/src/unicorn/hooks.lua b/src/unicorn/hooks.lua new file mode 100644 index 00000000..30cd75c2 --- /dev/null +++ b/src/unicorn/hooks.lua @@ -0,0 +1 @@ +--- @module hooks diff --git a/src/unicorn/init.lua b/src/unicorn/init.lua new file mode 100644 index 00000000..967c6c34 --- /dev/null +++ b/src/unicorn/init.lua @@ -0,0 +1,43 @@ +--- The main module for the Unicorn CPU Emulator. +--- @module unicorn + +local uc_c = require("unicorn_c_") +local uc_engine = require("unicorn.engine") + +local M = { + --- Determine if the given architecture is supported by this engine. + --- @tparam int architecture + --- An enum value indicating the architecture, from @{unicorn_const}. + --- These all start with "UC_ARCH_", e.g. + --- @{unicorn_const.UC_ARCH_X86}. + --- @treturn bool A boolean indicating if Unicorn supports the given architecture. + arch_supported = uc.arch_supported, + --- Return the error message corresponding to the given Unicorn error code. + strerror = uc.strerror, + version = uc.version, +} + + +--- Create a new Unicorn engine. +--- +--- @tparam int architecture +--- An enum value indicating which architecture to emulate. The constants are +--- available in the @{unicorn_const} module and all start with "UC_ARCH_". +--- @tparam int flags +--- Architecture-specific flags that control the engine's behavior. These can be used +--- to select a specific version of an architecture, endianness (for bi-endian +--- architectures), and so on. +--- +--- Flags are exposed in each architecture's `const` module. For example, the x86 +--- architecture's flags are in @{x86_const}, ARM64 in @{arm64_const}, +--- and so on. +--- @treturn Engine An initialized @{engine.Engine|Engine}. +function M.open(architecture, flags) + local handle, err = uc_c.open(architecture, flags or 0) + if err ~= nil then + return nil, err + end + return uc_engine.Engine(handle), nil +end + +return M diff --git a/tools/generate_constants.lua b/tools/generate_constants.lua index abd95cc6..236132d9 100644 --- a/tools/generate_constants.lua +++ b/tools/generate_constants.lua @@ -30,28 +30,34 @@ OUTPUT_CPP_TEMPLATE = [[ * * Source: $(header_file) * - * @file $(slug)_const.cpp + * @file $(slug)_const.c */ #include -! if slug ~= "unicorn" then +! if pl_tablex.size(constants) > 0 then #include ! end -#include "unicornlua/lua.hpp" -#include "unicornlua/utils.hpp" - -static constexpr struct NamedIntConst kConstants[] { +#include + +int main(void) +{ +! if pl_tablex.size(constants) > 0 then + printf( + "--- Constants exported by \"$(slug).h\".\n" \ + "--- For more information, consult the Unicorn Engine library's docs.\n" \ + "--- @module $(slug)_const\n" \ + "return {\n" + ); ! for name, text in pairs(constants) do - {"$(name)", $(name)}, + printf(" --- $(name)\n $(name) = %d;\n", $(name)); +! end + printf("}\n"); +! else + printf( + "error(\"Unicorn wasn't compiled with support for the `$(slug)' architecture.\")\n" + ); ! end - {nullptr, 0} -}; - -extern "C" UNICORN_EXPORT int luaopen_unicorn_$(slug)_const(lua_State *L) { - lua_createtable(L, 0, $(pl_tablex.size(constants))); - load_int_constants(L, kConstants); - return 1; } ]] diff --git a/tools/render_template.lua b/tools/render_template.lua index 27149118..ec487153 100644 --- a/tools/render_template.lua +++ b/tools/render_template.lua @@ -27,8 +27,8 @@ Render a template. Things to note: o The Lua line escape character is `@', not the default `#'. This prevents the template engine from mistaking a C/C++ preprocessor directive for a - line comment. - o The inline escape is the default $(...) + line comment. This can be overridden. + o The inline escape is the default $(...) but can be overridden. o The template and values files are executed in a mostly-empty environment, except for the following functions and tables: @@ -43,6 +43,12 @@ Arguments: A string variable to define, either "X" (empty string) or "X=YZ" (variable X assigned to "YZ"). Variables defined on the command line override values given by the values file. + -e,--escape (default "@") + Override the character that escapes a line into Lua. + -i,--inline-escape (default "$()") + Override the inline escape form. This must be three characters, + corresponding to the escape character, the opening bracket, and the + closing bracket. The brackets should be different characters. -o (default stdout) The file to write the rendered template to.