Enable offlineasm debug annotations for GCC
https://bugs.webkit.org/show_bug.cgi?id=207119

Patch by Angelos Oikonomopoulos <angelos@igalia.com> on 2020-04-02
Reviewed by Darin Adler.

This simply reuses the existing code that generates debug
annotations, adding two workarounds for limitations in GCC and
GDB.

First, the .file directives that offlineasm inserts in inline asm
use file slots that conflict with those in the compilation unit
that includes LLIntAssembly.h (specifically,
LowLevelInterpreter.cpp). Clang's built-in assembler will
transparently fix that for us, but for GCC we need to
post-process the generated assembler.

Unfortunately, cmake doesn't allow us to introduce a compiler wrapper for a
single source file, so we need to create a separate target for it.  This
wrapping only happens when building with GCC and the user has explicitly
requested debug information, either by selecting a Debug/RelWithDebInfo build
or setting GCC_OFFLINEASM_SOURCE_MAP.

Second, GDB will only look at the line table for a compilation unit if
it can first resolve the address to one of the known symbols in the
file. Introduce marker symbols to work around this bug.

* CMakeLists.txt:

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@259390 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt
index 3ff225e..791b7fe 100644
--- a/Source/JavaScriptCore/CMakeLists.txt
+++ b/Source/JavaScriptCore/CMakeLists.txt
@@ -323,7 +323,7 @@
     OUTPUT ${JavaScriptCore_DERIVED_SOURCES_DIR}/${LLIntOutput}
     MAIN_DEPENDENCY ${JAVASCRIPTCORE_DIR}/offlineasm/asm.rb
     DEPENDS LLIntOffsetsExtractor ${LLINT_ASM} ${OFFLINE_ASM} ${JavaScriptCore_DERIVED_SOURCES_DIR}/InitBytecodes.asm ${JavaScriptCore_DERIVED_SOURCES_DIR}/InitWasm.asm
-    COMMAND ${RUBY_EXECUTABLE} ${JAVASCRIPTCORE_DIR}/offlineasm/asm.rb -I${JavaScriptCore_DERIVED_SOURCES_DIR}/ ${JAVASCRIPTCORE_DIR}/llint/LowLevelInterpreter.asm $<TARGET_FILE:LLIntOffsetsExtractor> ${JavaScriptCore_DERIVED_SOURCES_DIR}/${LLIntOutput} ${OFFLINE_ASM_ARGS}
+    COMMAND ${CMAKE_COMMAND} -E env CMAKE_CXX_COMPILER_ID=${CMAKE_CXX_COMPILER_ID} GCC_OFFLINEASM_SOURCE_MAP=${GCC_OFFLINEASM_SOURCE_MAP} ${RUBY_EXECUTABLE} ${JAVASCRIPTCORE_DIR}/offlineasm/asm.rb -I${JavaScriptCore_DERIVED_SOURCES_DIR}/ ${JAVASCRIPTCORE_DIR}/llint/LowLevelInterpreter.asm $<TARGET_FILE:LLIntOffsetsExtractor> ${JavaScriptCore_DERIVED_SOURCES_DIR}/${LLIntOutput} ${OFFLINE_ASM_ARGS}
     COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${JavaScriptCore_DERIVED_SOURCES_DIR}/${LLIntOutput}
     WORKING_DIRECTORY ${JavaScriptCore_DERIVED_SOURCES_DIR}
     VERBATIM)
@@ -348,10 +348,17 @@
         COMMAND ${MASM_EXECUTABLE} ${LLINT_MASM_FLAGS} ${JavaScriptCore_DERIVED_SOURCES_DIR}/LowLevelInterpreterWin.obj ${JavaScriptCore_DERIVED_SOURCES_DIR}/LowLevelInterpreterWin.asm
         VERBATIM)
     list(APPEND JavaScriptCore_SOURCES ${JavaScriptCore_DERIVED_SOURCES_DIR}/LowLevelInterpreterWin.obj)
+    add_library(LowLevelInterpreterLib OBJECT llint/LowLevelInterpreter.cpp)
 else ()
-    list(APPEND JavaScriptCore_HEADERS
-        ${JavaScriptCore_DERIVED_SOURCES_DIR}/LLIntAssembly.h
-    )
+    # As there's poor toolchain support for using `.file` directives in
+    # inline asm (i.e. there's no way to avoid clashes with the `.file`
+    # directives generated by the C code in the compilation unit), we
+    # introduce a postprocessing pass for the asm that gets assembled into
+    # an object file. We only need to do this for LowLevelInterpreter.cpp
+    # and cmake doesn't allow us to introduce a compiler wrapper for a
+    # single source file, so we need to create a separate target for it.
+    add_library(LowLevelInterpreterLib OBJECT llint/LowLevelInterpreter.cpp
+        ${JavaScriptCore_DERIVED_SOURCES_DIR}/${LLIntOutput})
 endif ()
 
 # WebAssembly generator
@@ -1362,6 +1369,29 @@
 
 add_subdirectory(shell)
 
+target_include_directories(LowLevelInterpreterLib
+    PRIVATE "$<TARGET_PROPERTY:JavaScriptCore,INCLUDE_DIRECTORIES>")
+
+if (CMAKE_COMPILER_IS_GNUCXX AND GCC_OFFLINEASM_SOURCE_MAP)
+    message(STATUS "Enabling asm postprocessing")
+
+    set(LowLevelInterpreter_LAUNCHER "${JavaScriptCore_SCRIPTS_SOURCES_DIR}/postprocess-asm")
+    get_target_property(PROP_RULE_LAUNCH_COMPILE LowLevelInterpreterLib RULE_LAUNCH_COMPILE)
+    if (PROP_RULE_LAUNCH_COMPILE)
+        set(LowLevelInterpreter_LAUNCHER "${LowLevelInterpreter_LAUNCHER} ${PROP_RULE_LAUNCH_COMPILE}")
+    endif ()
+    set_property(TARGET LowLevelInterpreterLib
+        PROPERTY RULE_LAUNCH_COMPILE "${LowLevelInterpreter_LAUNCHER}")
+
+    # Pass in the filename as a magic preprocessor directive, so that
+    # the wrapper can accurately identify the source file.
+    set_source_files_properties("llint/LowLevelInterpreter.cpp"
+        PROPERTIES
+        COMPILE_DEFINITIONS "POSTPROCESS_ASM=llint/LowLevelInterpreter.cpp")
+endif ()
+
+list(APPEND JavaScriptCore_LIBRARIES LowLevelInterpreterLib)
+
 WEBKIT_COMPUTE_SOURCES(JavaScriptCore)
 WEBKIT_WRAP_SOURCELIST(${JavaScriptCore_SOURCES})
 WEBKIT_FRAMEWORK(JavaScriptCore)
diff --git a/Source/JavaScriptCore/ChangeLog b/Source/JavaScriptCore/ChangeLog
index c8b58ea..fda1cdf 100644
--- a/Source/JavaScriptCore/ChangeLog
+++ b/Source/JavaScriptCore/ChangeLog
@@ -1,3 +1,33 @@
+2020-04-02  Angelos Oikonomopoulos  <angelos@igalia.com>
+
+        Enable offlineasm debug annotations for GCC
+        https://bugs.webkit.org/show_bug.cgi?id=207119
+
+        Reviewed by Darin Adler.
+
+        This simply reuses the existing code that generates debug
+        annotations, adding two workarounds for limitations in GCC and
+        GDB.
+
+        First, the .file directives that offlineasm inserts in inline asm
+        use file slots that conflict with those in the compilation unit
+        that includes LLIntAssembly.h (specifically,
+        LowLevelInterpreter.cpp). Clang's built-in assembler will
+        transparently fix that for us, but for GCC we need to
+        post-process the generated assembler.
+
+        Unfortunately, cmake doesn't allow us to introduce a compiler wrapper for a
+        single source file, so we need to create a separate target for it.  This
+        wrapping only happens when building with GCC and the user has explicitly
+        requested debug information, either by selecting a Debug/RelWithDebInfo build
+        or setting GCC_OFFLINEASM_SOURCE_MAP.
+
+        Second, GDB will only look at the line table for a compilation unit if
+        it can first resolve the address to one of the known symbols in the
+        file. Introduce marker symbols to work around this bug.
+
+        * CMakeLists.txt:
+
 2020-04-01  Ross Kirsling  <ross.kirsling@sony.com>
 
         Intl.NumberFormat.prototype.format must preserve sign of -0
diff --git a/Source/JavaScriptCore/Scripts/postprocess-asm b/Source/JavaScriptCore/Scripts/postprocess-asm
new file mode 100755
index 0000000..df0a312
--- /dev/null
+++ b/Source/JavaScriptCore/Scripts/postprocess-asm
@@ -0,0 +1,193 @@
+#!/usr/bin/env ruby
+
+# Copyright (C) 2020 Igalia S. L.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+# THE POSSIBILITY OF SUCH DAMAGE.
+
+# Wrapper for a .cpp -> .o compilation command. It
+# 1. converts the command to generate a `.s' file
+# 2. runs the ASM postprocessor on it to generate the final `.s` file
+# 3. assembles the `.s` file to a `.o` file
+
+$asm_suffix_pre = ".pre.s"
+$asm_suffix = ".s"
+$postprocessor = "#{File.dirname($0)}/resolve-asm-file-conflicts.rb"
+
+
+$intermediate_paths = []
+
+# We need to work with indices a lot and unfortunately 'getoptlong' in
+# the standard library doesn't expose optind, so we're going with
+# array searches for simplicity.
+def index_nofail(ary, f, errmsg)
+  idx = ary.index { |el|
+    f.call(el)
+  }
+  if idx.nil?
+    $stderr.puts(errmsg)
+    exit(3)
+  end
+  idx
+end
+
+# Find and return the source file for this compilation command,
+# removing it from the args. Note that the path (A) as it appears here
+# (coming from a cmake rule) is likely to be different to the
+# anonymous argument (B) to the compilation command. However, A will
+# be a suffix of B.
+#
+# Exit with an error if the argument is not there. This has already
+# been checked by `cxx-wrapper`, otherwise we wouldn't be running.
+def extract_input!(args)
+  prefix = '-DPOSTPROCESS_ASM='
+
+  idx = index_nofail(args, Proc.new { |arg|
+    arg.start_with?(prefix)
+  }, "No `-DPOSTPROCESS_ASM` argument`")
+
+  path = args[idx][prefix.size..-1]
+  if path.size == 0
+    $stderr.puts("Empty path in -DPOSTPROCESS_ASM=")
+    exit(3)
+  end
+  # We only need this to be defined for the preprocessor (not any
+  # wrapper) from now on.
+  args[idx] = "-DPOSTPROCESS_ASM"
+  return path
+end
+
+# Get the index of the first argument ending in this suffix.
+#
+# Exit with an error if the argument isn't there. We're only ever
+# called with arguments we know are being passed in by the build
+# system.
+def get_arg_idx_suffix(args, wanted)
+  index_nofail(args, Proc.new { |arg|
+                 arg.end_with?(wanted)
+               }, "No argument ends with #{wanted}")
+end
+
+# Get index of a given argument. Die if it's not there.
+def get_arg_idx(args, wanted)
+  index_nofail(args, Proc.new { |arg|
+                 arg == wanted
+               }, "No `#{wanted}` argument")
+end
+
+# Get the index of `-o` and verify that an argument follows.
+# Both are guaranteed to exist (from our build system).
+def get_o_idx(args)
+  i = get_arg_idx(args, '-o')
+  if (i + 1) >= args.size
+    $stderr.puts("No argument to `-o`")
+    exit(3)
+  end
+  i
+end
+
+# Run command and die if it fails, propagating the exit code.
+def run_cmd(cmd)
+  pid = Process.spawn(*cmd)
+  Process.waitpid(pid)
+  ret = $?
+  if not ret.success?
+    $stderr.puts("Error running cmd: #{ret}")
+    exit(ret.exitstatus)
+  end
+end
+
+# Convert
+#    cxx -o blah.o -c blah.cpp
+# to
+#    cxx -o blah.s -S blah.cpp
+def build_cxx_cmd(args)
+  c_idx = get_arg_idx(args, '-c')
+  o_idx = get_o_idx(args)
+
+  cxx_args = args.clone
+
+  cxx_args[c_idx] = '-S'
+  o_path = cxx_args[o_idx + 1]
+  cxx_args[o_idx + 1] = o_path.sub(/[.]o$/, $asm_suffix)
+  $intermediate_paths << cxx_args[o_idx + 1]
+  if cxx_args[o_idx + 1] == o_path
+    $stderr.puts("Output file name not an object file: `#{o_path}`")
+    exit(3)
+  end
+  cxx_args
+end
+
+# Do
+#     mv blah.S blah.pre.S
+# The reason we do a rename instead of directly generating the .pre.s
+# file when compiling is so that the corresponding .dwo file will have
+# the correct name embedded.
+def rename_s_file(args)
+  o_path = args[get_o_idx(args) + 1]
+  File.rename(o_path.sub(/[.]o$/, $asm_suffix),
+              o_path.sub(/[.]o$/, $asm_suffix_pre))
+end
+
+# Build
+#     postprocessor blah.pre.S blah.S
+def build_postprocessor_cmd(args)
+  o_path = args[get_o_idx(args) + 1]
+
+  pp_args = [
+    $postprocessor,
+    o_path.sub(/[.]o$/, $asm_suffix_pre), # input
+    o_path.sub(/[.]o$/, $asm_suffix) # output
+  ]
+  $intermediate_paths << pp_args[-2]
+  $intermediate_paths << pp_args[-1]
+  pp_args
+end
+
+# Build
+#     cxx -o blah.o -c blah.S
+def build_as_cmd(args, i_path)
+  i_idx = get_arg_idx_suffix(args, i_path)
+  o_path = args[get_o_idx(args) + 1]
+
+  as_args = args.clone
+  i_path = as_args[i_idx]
+  as_args[i_idx] = o_path.sub(/[.]o$/, $asm_suffix)
+  as_args
+end
+
+args = ARGV.to_a
+i_path = extract_input!(args)
+
+begin
+  run_cmd(build_cxx_cmd(args))
+  rename_s_file(args)
+  run_cmd(build_postprocessor_cmd(args))
+  run_cmd(build_as_cmd(args, i_path))
+ensure
+  $intermediate_paths.each { |p|
+    if File.exist?(p)
+      File.delete(p)
+    end
+  }
+end
+
+exit(0)
diff --git a/Source/JavaScriptCore/Scripts/resolve-asm-file-conflicts.rb b/Source/JavaScriptCore/Scripts/resolve-asm-file-conflicts.rb
new file mode 100755
index 0000000..21832d8
--- /dev/null
+++ b/Source/JavaScriptCore/Scripts/resolve-asm-file-conflicts.rb
@@ -0,0 +1,399 @@
+#!/usr/bin/env ruby
+
+# Copyright (C) 2020 Igalia S. L.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+# THE POSSIBILITY OF SUCH DAMAGE.
+
+# Resolve conflicts in `.file` directives in assembler files. Those
+# may result from inline asm statements which themselves have `.file`
+# directives. As the inline asm has no idea what the next free file
+# number is, it'll generally have its own numbering. To get things to
+# work we have to
+# 1. remap conflicting file numbers
+# 2. change `.loc` directives to reference the appropriate file number.
+#
+# To be able to do that, we need some concept of "scope", i.e. which
+# set of files a given `.loc` directive refers to. We get that by
+# tracking the #APP/#NOAPP directives that the compiler emits when it
+# switch to/from inline asm.
+#
+# In effect, we convert
+#     .file 1 "foo"
+#     #APP
+#     .file 1 "bar"
+#     .file 2 "foo"
+#     .loc 1, X
+#     #NOAPP
+#     .loc 1, Y
+# to
+#     .file 1 "foo"
+#     #APP
+#     .file 2 "bar"
+#     .file 1 "foo"
+#     .loc 2, X
+#     #NOAPP
+#     .loc 1, Y
+
+require 'pathname'
+require 'stringio'
+require 'strscan'
+
+ParseResultSuccess = Struct.new(:str)
+ParseResultError = Struct.new(:error)
+
+# Parses the single string literal following a .file assembler directive
+class FileDirectiveArgScanner
+  def initialize(s)
+    @s = StringScanner.new(s)
+  end
+  def parse
+    @s.skip(/\s*/)
+
+    # We require at least one string literal
+    ret = parse_string_literal
+    if ret.respond_to?(:error)
+      return ret
+    end
+
+    @s.skip(/\s*/)
+    if not @s.eos?
+      return ParseResultError.new("Expected end of line after #{ret.str}")
+    end
+    return ParseResultSuccess.new(ret.str)
+  end
+  def parse_string_literal
+    if @s.scan(/"/).nil?
+      err = "Expected string literal at `#{@s.string}` (pos #{@s.pos})"
+      return ParseResultError.new(err)
+    end
+    parse_until_end_of_string_literal
+  end
+  def parse_until_end_of_string_literal
+    start_pos = @s.pos
+    while true
+      # Search for our special characters
+      @s.skip(/[^"\\]+/)
+      if @s.scan(/\\/)
+        if @s.scan(/\\/)
+          # When we see '\\', consume both characters so that the
+          # second '\' will not be treated as an escape char.
+          next
+        elsif @s.scan(/"/)
+          # For '\"', consume both characters so that the '"' will not
+          # terminate the string.
+          next
+        end
+        next
+      elsif @s.scan(/"/)
+        # '"' without a preceeding '\'; terminate the literal.
+        # We're already past the '"', so the literal ends at -2
+        # characters.
+        return ParseResultSuccess.new(@s.string[start_pos..(@s.pos - 2)])
+      elsif @s.eos?
+        err = "Unterminated string literal (starting at pos #{start_pos} in #{@s.string}"
+        return ParseResultError.new(err)
+      end
+      raise "Internal error (#{@s.inspect})"
+    end
+  end
+end
+
+def test(outf, str, res)
+  pr = FileDirectiveArgScanner.new(str).parse
+  if res.is_a?(Array)
+    if pr.respond_to?(:error)
+      outf.puts("Parse result is `#{pr.error}` but expected #{res}")
+      return false
+    end
+    if pr.str != res[0]
+      outf.puts("Parsed path `#{pr.str}` but expected `#{res[0]}`")
+      return false
+    end
+    return true
+  elsif res.is_a?(String)
+    if pr.respond_to?(:error)
+      if not pr.error.downcase.include?(res.downcase)
+        err = "Error message `#{pr.error}` does not include expected substring `#{res}`"
+        outf.puts(err)
+        return false
+      end
+      return true
+    end
+    outf.puts("Expected error (#{res}) but got successful parse #{pr.str}")
+    return false
+  else
+    raise "Internal error #{res.class}"
+  end
+end
+
+def selftest
+  nr_succeeded = 0
+  tests = [
+    # simple string
+    ['"foo/bar"', ["foo/bar"]],
+
+    ['"foo', "Unterminated string literal"],
+
+    ['"foo\"', "Unterminated string literal"],
+
+    # "foo\"
+    ["\"foo\\\"", "Unterminated string literal"],
+
+    # "foo\\"
+    ["\"foo\\\\\"", ["foo\x5c\x5c"]],
+
+    # Can escape '"'
+    ['"foo\"bar"', ['foo\"bar']],
+
+    # Can parse relative
+    ['"foo/bar"', ['foo/bar']],
+
+    # Can parse absolute
+    ['"/bar"', ['/bar']],
+
+    # Can parse absolute with two components
+    ['"/bar/baz"', ['/bar/baz']],
+
+    # Can detect stray token
+    ['"foo" bar', "Expected end of line"],
+
+    # Can detect stray token without whitespace
+    ['"foo"bar', "Expected end of line"],
+
+    # Will not accept clang-style .file directives
+    ['"working_directory" "path"', "Expected end of line"]
+  ]
+  outf = StringIO.new("")
+  tests.each { |str, res|
+    if test(outf, str, res)
+      nr_succeeded += 1
+    end
+  }
+  if nr_succeeded != tests.size
+    $stderr.puts(outf.string)
+    $stderr.puts("Some self tests failed #{nr_succeeded}/#{tests.size}")
+    exit(3)
+  end
+end
+
+# Keep track of which fileno is assigned to which file path. We call
+# the fileno a 'slot'.
+class FileSlotTracker
+  @@next_free = 1   # Next free global slot
+  @@paths = {} # Maps path -> global slot
+  def initialize(parent)
+    @parent = parent
+    # This maps from our own local slots (i.e. within an APP section)
+    # to a global slot.
+    @slot_map = {}
+  end
+  # We make sure that .file directives that specify the same path are
+  # dedup'd, i.e. if we see
+  #     .file N "/path/to/include.h"
+  #     ...
+  #     .file M "/path/to/include.h"
+  # then the "local" N, M slots will be mapped to the same "global" slot.
+  def register_path(path, slot)
+    curr_slot = @@paths[path]
+    if curr_slot.nil?
+      # We haven't seen this file before
+      if slot <= @@next_free
+        # Desired file slot either clashes with an existing one, or is
+        # the next one to be allocated. In either case, assign the
+        # next free slot.
+        assign_next_slot(path)
+      else
+        # Don't allow slot gaps. Technically we could handle them, but
+        # they should never happen; bail now rather than papering over
+        # an earlier error.
+        $stderr.puts("File wants slot #{slot} but only seen #{@@next_free} so far")
+        exit(2)
+      end
+    else
+      # We've already assigned a slot for this file.
+    end
+    @slot_map[slot] = @@paths[path]
+    if @slot_map[slot].nil?
+      raise "Didn't map local slot #{slot}"
+    end
+  end
+  # Return global slot for path
+  def slot_for_path(path)
+    return @@paths[path]
+  end
+  # Return global slot that will replace the local slot
+  def remap_slot(slot)
+    ret = nil
+    if @slot_map.size > 0
+      # If the current NO_APP segment has defined a .file, only look
+      # in the current FileSlotTracker. This is the case for a
+      # top-level inline asm statement.
+      ret = @slot_map[slot]
+    elsif not @parent.nil?
+      # If the current NO_APP segment has not defined a .file, clearly
+      # all .loc directives refer to files defined in the APP
+      # part. This is the case for non-top-level inline asm
+      # statements.
+      ret = @parent.remap_slot(slot)
+    end
+    if ret.nil?
+      raise "No global slot for #{slot}"
+    end
+    ret
+  end
+  private
+  def assign_next_slot(path)
+    @@paths[path] = @@next_free
+    @@next_free += 1
+  end
+end
+
+# Return sequential lines, while keeping track of whether we're in an
+# #APP or #NOAPP section.
+class AsmReader
+  attr_reader :in_app
+  attr_accessor :app_to_noapp, :noapp_to_app
+  def initialize(f)
+    @f = f
+    @f.rewind
+    @linenr = 0
+    @in_app = false
+    @last_switch = "start of file" # For error messages
+  end
+  def next_line
+    while true
+      l = @f.gets
+      if l.nil?
+        return l
+      end
+      @linenr += 1
+      if /^#\s*APP\s*/.match(l)
+        if @in_app
+          raise "#APP on line #{@linenr} but already in #APP (#{@last_switch})"
+        end
+        @in_app = true
+        @last_switch = @linenr
+        if @noapp_to_app
+          @noapp_to_app.call()
+        end
+      end
+      if /^#\s*NO_APP\s*/.match(l)
+        if not @in_app
+          raise "#NOAPP on line #{@linenr} but was not in #APP (last swich at #{@last_switch})"
+        end
+        @in_app = false
+        @last_switch = @linenr
+        if @app_to_noapp
+          @app_to_noapp.call()
+        end
+      end
+      return l
+    end
+  end
+end
+
+class FileConflictResolver
+  @@file_re = /^\s*[.]file\s+(?<slot>\d+)\s+(?<rest>.*)$/
+  @@loc_re = /^(?<white1>\s*)[.]loc(?<white2>\s+)(?<slot>\d+)(?<rest>\s+\d+.*)$/
+  def initialize(inf, outf)
+    @outf = outf
+    @trackers = [FileSlotTracker.new(nil)]
+    @asm_reader = AsmReader.new(inf)
+    # When we enter an #APP section (i.e. asm emitted from an expanded
+    # inline asm statement), create a new file tracker, in effect
+    # processing the .file and .loc directives in a new "namespace"
+    # (as far as the file numbers are concerned). This is an array,
+    # but in practice the size will either be 1 (NOAPP) or 2 (APP).
+    @asm_reader.app_to_noapp = Proc.new { ||
+                                          @trackers.pop
+    }
+    @asm_reader.noapp_to_app = Proc.new { ||
+                                          @trackers.push(FileSlotTracker.new(@trackers[-1]))
+    }
+  end
+  def run
+    while true
+      l = @asm_reader.next_line
+      break unless l
+
+      md = @@file_re.match(l)
+      if md
+        file_directive(md)
+        next
+      end
+      md = @@loc_re.match(l)
+      if md
+        loc_directive(md)
+        next
+      end
+      @outf.write(l)
+      next
+    end
+  end
+  def loc_directive(md)
+    tracker = @trackers.last
+    slot = tracker.remap_slot(md[:slot].to_i)
+    @outf.puts("#{md[:white1]}.loc#{md[:white2]}#{slot}#{md[:rest]}")
+  end
+  def file_directive(md)
+    tracker = @trackers.last
+
+    pr = FileDirectiveArgScanner.new(md[:rest]).parse
+    if pr.respond_to?(:error)
+      $stderr.puts("Error parsing path argument to .file directive: #{pr.error}")
+      exit(2)
+    end
+
+    path = pr.str
+    tracker.register_path(path, md[:slot].to_i)
+
+    slot = tracker.slot_for_path(path)
+    @outf.puts("\t.file\t#{slot} #{md[:rest]}")
+  end
+end
+
+# First, make sure our tests still pass. This only takes a fraction of
+# our runtime and ensures the tests will get run by anyone trying out
+# changes to this file.
+selftest
+
+if ARGV.size != 2
+  $stderr.puts("Usage: #{$0} input output")
+  exit(2)
+end
+
+inpath, outpath = ARGV.collect { |n| Pathname.new(n) }
+
+if not inpath.file?
+  $stderr.puts("Not a regular file: `#{inpath}`")
+  exit(2)
+end
+
+if inpath.extname.upcase != ".S"
+  $stderr.puts("warning: file `#{inpath}` doesn't have a `.s` or `.S` extension. Going on anyway...")
+end
+
+File.open(inpath, "r") { |inf|
+  File.open(outpath, "w") { |outf|
+    FileConflictResolver.new(inf, outf).run
+  }
+}
diff --git a/Source/JavaScriptCore/Sources.txt b/Source/JavaScriptCore/Sources.txt
index c7b638b..e6deb01 100644
--- a/Source/JavaScriptCore/Sources.txt
+++ b/Source/JavaScriptCore/Sources.txt
@@ -675,8 +675,6 @@
 llint/LLIntExceptions.cpp
 llint/LLIntSlowPaths.cpp
 llint/LLIntThunks.cpp
-// FIXME: We can't bundle this file due to a clang bug.
-llint/LowLevelInterpreter.cpp @no-unify
 
 parser/Lexer.cpp
 parser/ModuleAnalyzer.cpp
diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp b/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp
index f8afc7a..c9aafc1 100644
--- a/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp
+++ b/Source/JavaScriptCore/llint/LowLevelInterpreter.cpp
@@ -537,8 +537,19 @@
 #define OFFLINE_ASM_OPCODE_DEBUG_LABEL(label)
 #endif
 
+// This works around a bug in GDB where, if the compilation unit
+// doesn't have any address range information, its line table won't
+// even be consulted. Emit {before,after}_llint_asm so that the code
+// emitted in the top level inline asm statement is within functions
+// visible to the compiler. This way, GDB can resolve a PC in the
+// llint asm code to this compilation unit and the successfully look
+// up the line number information.
+DEBUGGER_ANNOTATION_MARKER(before_llint_asm)
+
 // This is a file generated by offlineasm, which contains all of the assembly code
 // for the interpreter, as compiled from LowLevelInterpreter.asm.
 #include "LLIntAssembly.h"
 
+DEBUGGER_ANNOTATION_MARKER(after_llint_asm)
+
 #endif // ENABLE(C_LOOP)
diff --git a/Source/JavaScriptCore/offlineasm/config.rb b/Source/JavaScriptCore/offlineasm/config.rb
index 49543fc..2f3123e 100644
--- a/Source/JavaScriptCore/offlineasm/config.rb
+++ b/Source/JavaScriptCore/offlineasm/config.rb
@@ -89,6 +89,10 @@
                 end
             end
         end
+    elsif ENV["CMAKE_CXX_COMPILER_ID"] == 'GNU' and
+         ENV["GCC_OFFLINEASM_SOURCE_MAP"] == 'ON'
+      # All GCC versions that can build JSC support debug annotations
+      return true
     end
 
     false
diff --git a/Source/WTF/wtf/Compiler.h b/Source/WTF/wtf/Compiler.h
index 4374cb3..0fb3320 100644
--- a/Source/WTF/wtf/Compiler.h
+++ b/Source/WTF/wtf/Compiler.h
@@ -382,6 +382,17 @@
 #define WARN_UNUSED_RETURN
 #endif
 
+/* DEBUGGER_ANNOTATION_MARKER */
+
+#if !defined(DEBUGGER_ANNOTATION_MARKER) && COMPILER(GCC)
+#define DEBUGGER_ANNOTATION_MARKER(name) \
+    __attribute__((__no_reorder__)) void name(void) { __asm__(""); }
+#endif
+
+#if !defined(DEBUGGER_ANNOTATION_MARKER)
+#define DEBUGGER_ANNOTATION_MARKER(name)
+#endif
+
 #if !defined(__has_include) && COMPILER(MSVC)
 #define __has_include(path) 0
 #endif
diff --git a/Source/cmake/OptionsCommon.cmake b/Source/cmake/OptionsCommon.cmake
index a574ef8..ea13624 100644
--- a/Source/cmake/OptionsCommon.cmake
+++ b/Source/cmake/OptionsCommon.cmake
@@ -112,6 +112,15 @@
     set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gdb-index")
 endif ()
 
+set(GCC_OFFLINEASM_SOURCE_MAP_DEFAULT OFF)
+if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
+    set(GCC_OFFLINEASM_SOURCE_MAP_DEFAULT ON)
+endif ()
+
+option(GCC_OFFLINEASM_SOURCE_MAP
+  "Produce debug line information for offlineasm-generated code"
+  ${GCC_OFFLINEASM_SOURCE_MAP_DEFAULT})
+
 # Enable the usage of OpenMP.
 #  - At this moment, OpenMP is only used as an alternative implementation
 #    to native threads for the parallelization of the SVG filters.