diff --git a/src/lua/init.c b/src/lua/init.c
index b6ea368eca7b6becd4ef382aa463c12fa3aae248..456560592c37c1fb54cdbfed11fd98b22af6493c 100644
--- a/src/lua/init.c
+++ b/src/lua/init.c
@@ -35,6 +35,7 @@
 #include <libgen.h>
 #endif
 
+#include <assert.h>
 #include <lua.h>
 #include <lauxlib.h>
 #include <lualib.h>
@@ -89,6 +90,8 @@ LUALIB_API int
 luaopen_zip(lua_State *L);
 #endif
 
+#define MAX_MODNAME 64
+
 /**
  * The single Lua state of the transaction processor (tx) thread.
  */
@@ -424,6 +427,50 @@ static const char *lua_modules_preload[] = {
  * {{{ box Lua library: common functions
  */
 
+/*
+ * Retrieve builtin module sources, if available.
+ */
+static const char *
+tarantool_debug_getsources(const char *modname)
+{
+	for (size_t i = 0; lua_modules[i] != NULL; i += 2) {
+		const char *shortname = lua_modules[i];
+		const char *lua_code = lua_modules[i + 1];
+		assert(lua_code != NULL);
+		assert(strlen(shortname) + sizeof("@builtin/.lua") <=
+			MAX_MODNAME);
+		char fullname[MAX_MODNAME];
+		snprintf(fullname, sizeof(fullname), "@builtin/%s.lua",
+			 shortname);
+		if (!strcmp(shortname, modname) || !strcmp(fullname, modname))
+			return lua_code;
+	}
+	return NULL;
+}
+
+/*
+ * LuaC implementation of a function to retrieve builtin module sources.
+ */
+static int
+lbox_tarantool_debug_getsources(struct lua_State *L)
+{
+	int index = lua_gettop(L);
+	if (index != 1)
+		luaL_error(L, "getsources() function expects one argument");
+	size_t len = 0;
+	const char *modname = luaL_checklstring(L, index, &len);
+	if (len <= 0)
+		goto ret_nil;
+	const char *code = tarantool_debug_getsources(modname);
+	if (code == NULL)
+		goto ret_nil;
+	lua_pushstring(L, code);
+	return 1;
+ret_nil:
+	lua_pushnil(L);
+	return 1;
+}
+
 /**
  * Convert lua number or string to lua cdata 64bit number.
  */
@@ -707,6 +754,13 @@ luaopen_tarantool(lua_State *L)
 	lua_settable(L, -3);
 
 	lua_settable(L, -3);    /* box.info.build */
+
+	/* debug */
+	lua_newtable(L);
+	lua_pushcfunction(L, lbox_tarantool_debug_getsources);
+	lua_setfield(L, -2, "getsources");
+	lua_setfield(L, -2, "debug");
+	lua_pop(L, 1);
 	return 1;
 }
 
@@ -806,7 +860,6 @@ tarantool_lua_init(const char *tarantool_bin, int argc, char **argv)
 	lua_pop(L, 1); /* _PRELOAD */
 
 	luaopen_tarantool(L);
-	lua_pop(L, 1);
 
 	lua_newtable(L);
 	lua_pushinteger(L, -1);
diff --git a/test/app-luatest/tnt_debug_getsources_test.lua b/test/app-luatest/tnt_debug_getsources_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..3658a6f89c3a77da6d59b0e56806c88e0810eada
--- /dev/null
+++ b/test/app-luatest/tnt_debug_getsources_test.lua
@@ -0,0 +1,50 @@
+local t = require('luatest')
+local fio = require('fio')
+local g = t.group()
+
+
+local function readfile(filename)
+    local f = assert(io.open(filename, "rb"))
+    return f:read('*a')
+end
+
+local files = {
+    'strict',
+    'debug',
+    'errno',
+    'fiber',
+    'env',
+    'datetime',
+}
+
+-- calculate reporsitory root using directory of a current
+-- script, but get 2 directories above
+local function repo_root()
+    local myself = fio.abspath(debug.getinfo(1,'S').source:gsub("^@", ""))
+    local root = fio.abspath(fio.dirname(myself) .. '/../..')
+    return root
+end
+
+g.test_tarantool_debug_getsources = function()
+    local git_root = repo_root()
+    t.assert_is_not(git_root, nil)
+    t.assert(fio.stat(git_root):is_dir())
+    local lua_src_dir = fio.pathjoin(git_root, '/src/lua')
+    t.assert_is_not(lua_src_dir, nil)
+    t.assert(fio.stat(lua_src_dir):is_dir())
+
+    local tnt = require('tarantool')
+    t.assert_is_not(tnt, nil)
+    local luadebug = tnt.debug
+    t.assert_is_not(luadebug, nil)
+
+    for _, file in pairs(files) do
+        local path = ('%s/%s.lua'):format(lua_src_dir, file)
+        local text = readfile(path)
+        t.assert_is_not(text, nil)
+        local source = luadebug.getsources(file)
+        t.assert_equals(source, text)
+        source = luadebug.getsources(('@builtin/%s.lua'):format(file))
+        t.assert_equals(source, text)
+    end
+end
diff --git a/third_party/lua/luadebug.lua b/third_party/lua/luadebug.lua
index d024d025fb47ca0247299770680a3005a14357a2..2ed213a56eeb73bd9becf7941e1c94abf1435c7b 100644
--- a/third_party/lua/luadebug.lua
+++ b/third_party/lua/luadebug.lua
@@ -248,14 +248,34 @@ end
 local SOURCE_CACHE = {}
 
 local function where(info, context_lines)
+    local filesource = info.source
     local source = SOURCE_CACHE[info.source]
     if not source then
         source = {}
-        local filename = info.source:match("@(.*)")
-        if filename then
-            pcall(function() for line in io.lines(filename) do table.insert(source, line) end end)
-        elseif info.source then
-            for line in info.source:gmatch("(.-)\n") do table.insert(source, line) end
+        -- Tarantool builtin module
+        if filesource:match("@builtin/.*.lua") then
+            pcall(function()
+                local tnt_debug = require('tarantool').debug
+                local lua_code = tnt_debug.getsources(filesource)
+
+                for line in string.gmatch(lua_code, "([^\n]*)\n?") do
+                    table.insert(source, line)
+                end
+            end)
+        else
+            -- external module - load file
+            local filename = filesource:match("@(.*)")
+            if filename then
+                pcall(function()
+                    for line in io.lines(filename) do
+                        table.insert(source, line)
+                    end
+                end)
+            elseif filesource then
+                for line in info.source:gmatch("(.-)\n") do
+                    table.insert(filesource, line)
+                end
+            end
         end
         SOURCE_CACHE[info.source] = source
     end