diff --git a/compiler/rustc_codegen_llvm/src/back/lto.rs b/compiler/rustc_codegen_llvm/src/back/lto.rs
index 29415973ed0..5effe687528 100644
--- a/compiler/rustc_codegen_llvm/src/back/lto.rs
+++ b/compiler/rustc_codegen_llvm/src/back/lto.rs
@@ -732,10 +732,7 @@ pub unsafe fn optimize_thin_module(
     let diag_handler = cgcx.create_diag_handler();
 
     let module_name = &thin_module.shared.module_names[thin_module.idx];
-    let split_dwarf_file = cgcx
-        .output_filenames
-        .split_dwarf_filename(cgcx.split_dwarf_kind, Some(module_name.to_str().unwrap()));
-    let tm_factory_config = TargetMachineFactoryConfig { split_dwarf_file };
+    let tm_factory_config = TargetMachineFactoryConfig::new(cgcx, module_name.to_str().unwrap());
     let tm =
         (cgcx.tm_factory)(tm_factory_config).map_err(|e| write::llvm_err(&diag_handler, &e))?;
 
diff --git a/compiler/rustc_codegen_llvm/src/back/write.rs b/compiler/rustc_codegen_llvm/src/back/write.rs
index e225730dce0..326ae354ccf 100644
--- a/compiler/rustc_codegen_llvm/src/back/write.rs
+++ b/compiler/rustc_codegen_llvm/src/back/write.rs
@@ -23,13 +23,11 @@ use rustc_fs_util::{link_or_copy, path_to_c_string};
 use rustc_hir::def_id::LOCAL_CRATE;
 use rustc_middle::bug;
 use rustc_middle::ty::TyCtxt;
-use rustc_session::config::{
-    self, Lto, OutputType, Passes, SanitizerSet, SplitDwarfKind, SwitchWithOptPath,
-};
+use rustc_session::config::{self, Lto, OutputType, Passes, SanitizerSet, SwitchWithOptPath};
 use rustc_session::Session;
 use rustc_span::symbol::sym;
 use rustc_span::InnerSpan;
-use rustc_target::spec::{CodeModel, RelocModel};
+use rustc_target::spec::{CodeModel, RelocModel, SplitDebuginfo};
 use tracing::debug;
 
 use libc::{c_char, c_int, c_uint, c_void, size_t};
@@ -93,9 +91,12 @@ pub fn create_informational_target_machine(sess: &Session) -> &'static mut llvm:
 }
 
 pub fn create_target_machine(tcx: TyCtxt<'_>, mod_name: &str) -> &'static mut llvm::TargetMachine {
-    let split_dwarf_file = tcx
-        .output_filenames(LOCAL_CRATE)
-        .split_dwarf_filename(tcx.sess.opts.debugging_opts.split_dwarf, Some(mod_name));
+    let split_dwarf_file = if tcx.sess.target_can_use_split_dwarf() {
+        tcx.output_filenames(LOCAL_CRATE)
+            .split_dwarf_filename(tcx.sess.split_debuginfo(), Some(mod_name))
+    } else {
+        None
+    };
     let config = TargetMachineFactoryConfig { split_dwarf_file };
     target_machine_factory(&tcx.sess, tcx.backend_optimization_level(LOCAL_CRATE))(config)
         .unwrap_or_else(|err| llvm_err(tcx.sess.diagnostic(), &err).raise())
@@ -838,11 +839,17 @@ pub(crate) unsafe fn codegen(
                     .generic_activity_with_arg("LLVM_module_codegen_emit_obj", &module.name[..]);
 
                 let dwo_out = cgcx.output_filenames.temp_path_dwo(module_name);
-                let dwo_out = match cgcx.split_dwarf_kind {
+                let dwo_out = match cgcx.split_debuginfo {
                     // Don't change how DWARF is emitted in single mode (or when disabled).
-                    SplitDwarfKind::None | SplitDwarfKind::Single => None,
+                    SplitDebuginfo::Off | SplitDebuginfo::Packed => None,
                     // Emit (a subset of the) DWARF into a separate file in split mode.
-                    SplitDwarfKind::Split => Some(dwo_out.as_path()),
+                    SplitDebuginfo::Unpacked => {
+                        if cgcx.target_can_use_split_dwarf {
+                            Some(dwo_out.as_path())
+                        } else {
+                            None
+                        }
+                    }
                 };
 
                 with_codegen(tm, llmod, config.no_builtins, |cpm| {
@@ -880,7 +887,7 @@ pub(crate) unsafe fn codegen(
 
     Ok(module.into_compiled_module(
         config.emit_obj != EmitObj::None,
-        cgcx.split_dwarf_kind == SplitDwarfKind::Split,
+        cgcx.target_can_use_split_dwarf && cgcx.split_debuginfo == SplitDebuginfo::Unpacked,
         config.emit_bc,
         &cgcx.output_filenames,
     ))
diff --git a/compiler/rustc_codegen_llvm/src/debuginfo/metadata.rs b/compiler/rustc_codegen_llvm/src/debuginfo/metadata.rs
index 3a4e1492af3..6e7c0b3e347 100644
--- a/compiler/rustc_codegen_llvm/src/debuginfo/metadata.rs
+++ b/compiler/rustc_codegen_llvm/src/debuginfo/metadata.rs
@@ -995,10 +995,13 @@ pub fn compile_unit_metadata(
     let flags = "\0";
 
     let out_dir = &tcx.output_filenames(LOCAL_CRATE).out_directory;
-    let split_name = tcx
-        .output_filenames(LOCAL_CRATE)
-        .split_dwarf_filename(tcx.sess.opts.debugging_opts.split_dwarf, Some(codegen_unit_name))
-        .unwrap_or_default();
+    let split_name = if tcx.sess.target_can_use_split_dwarf() {
+        tcx.output_filenames(LOCAL_CRATE)
+            .split_dwarf_filename(tcx.sess.split_debuginfo(), Some(codegen_unit_name))
+    } else {
+        None
+    }
+    .unwrap_or_default();
     let out_dir = out_dir.to_str().unwrap();
     let split_name = split_name.to_str().unwrap();
 
diff --git a/compiler/rustc_codegen_llvm/src/lib.rs b/compiler/rustc_codegen_llvm/src/lib.rs
index 92ac770aca5..d11c1592f99 100644
--- a/compiler/rustc_codegen_llvm/src/lib.rs
+++ b/compiler/rustc_codegen_llvm/src/lib.rs
@@ -351,12 +351,7 @@ impl ModuleLlvm {
         unsafe {
             let llcx = llvm::LLVMRustContextCreate(cgcx.fewer_names);
             let llmod_raw = back::lto::parse_module(llcx, name, buffer, handler)?;
-
-            let split_dwarf_file = cgcx
-                .output_filenames
-                .split_dwarf_filename(cgcx.split_dwarf_kind, Some(name.to_str().unwrap()));
-            let tm_factory_config = TargetMachineFactoryConfig { split_dwarf_file };
-
+            let tm_factory_config = TargetMachineFactoryConfig::new(&cgcx, name.to_str().unwrap());
             let tm = match (cgcx.tm_factory)(tm_factory_config) {
                 Ok(m) => m,
                 Err(e) => {
diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs
index 72e049b6d74..0738b2df71e 100644
--- a/compiler/rustc_codegen_ssa/src/back/link.rs
+++ b/compiler/rustc_codegen_ssa/src/back/link.rs
@@ -14,7 +14,7 @@ use rustc_session::utils::NativeLibKind;
 use rustc_session::{filesearch, Session};
 use rustc_span::symbol::Symbol;
 use rustc_target::spec::crt_objects::{CrtObjects, CrtObjectsFallback};
-use rustc_target::spec::{LinkOutputKind, LinkerFlavor, LldFlavor};
+use rustc_target::spec::{LinkOutputKind, LinkerFlavor, LldFlavor, SplitDebuginfo};
 use rustc_target::spec::{PanicStrategy, RelocModel, RelroLevel, Target};
 
 use super::archive::ArchiveBuilder;
@@ -99,9 +99,6 @@ pub fn link_binary<'a, B: ArchiveBuilder<'a>>(
                         path.as_ref(),
                         target_cpu,
                     );
-                    if sess.opts.debugging_opts.split_dwarf == config::SplitDwarfKind::Split {
-                        link_dwarf_object(sess, &out_filename);
-                    }
                 }
             }
             if sess.opts.json_artifact_notifications {
@@ -828,29 +825,43 @@ fn link_natively<'a, B: ArchiveBuilder<'a>>(
         }
     }
 
-    // On macOS, debuggers need this utility to get run to do some munging of
-    // the symbols. Note, though, that if the object files are being preserved
-    // for their debug information there's no need for us to run dsymutil.
-    if sess.target.is_like_osx
-        && sess.opts.debuginfo != DebugInfo::None
-        && !preserve_objects_for_their_debuginfo(sess)
-    {
-        let prog = Command::new("dsymutil").arg(out_filename).output();
-        match prog {
-            Ok(prog) => {
-                if !prog.status.success() {
-                    let mut output = prog.stderr.clone();
-                    output.extend_from_slice(&prog.stdout);
-                    sess.struct_warn(&format!(
-                        "processing debug info with `dsymutil` failed: {}",
-                        prog.status
-                    ))
-                    .note(&escape_string(&output))
-                    .emit();
+    match sess.split_debuginfo() {
+        // If split debug information is disabled or located in individual files
+        // there's nothing to do here.
+        SplitDebuginfo::Off | SplitDebuginfo::Unpacked => {}
+
+        // If packed split-debuginfo is requested, but the final compilation
+        // doesn't actually have any debug information, then we skip this step.
+        SplitDebuginfo::Packed if sess.opts.debuginfo == DebugInfo::None => {}
+
+        // On macOS the external `dsymutil` tool is used to create the packed
+        // debug information. Note that this will read debug information from
+        // the objects on the filesystem which we'll clean up later.
+        SplitDebuginfo::Packed if sess.target.is_like_osx => {
+            let prog = Command::new("dsymutil").arg(out_filename).output();
+            match prog {
+                Ok(prog) => {
+                    if !prog.status.success() {
+                        let mut output = prog.stderr.clone();
+                        output.extend_from_slice(&prog.stdout);
+                        sess.struct_warn(&format!(
+                            "processing debug info with `dsymutil` failed: {}",
+                            prog.status
+                        ))
+                        .note(&escape_string(&output))
+                        .emit();
+                    }
                 }
+                Err(e) => sess.fatal(&format!("unable to run `dsymutil`: {}", e)),
             }
-            Err(e) => sess.fatal(&format!("unable to run `dsymutil`: {}", e)),
         }
+
+        // On MSVC packed debug information is produced by the linker itself so
+        // there's no need to do anything else here.
+        SplitDebuginfo::Packed if sess.target.is_like_msvc => {}
+
+        // ... and otherwise we're processing a `*.dwp` packed dwarf file.
+        SplitDebuginfo::Packed => link_dwarf_object(sess, &out_filename),
     }
 }
 
@@ -1050,28 +1061,9 @@ fn preserve_objects_for_their_debuginfo(sess: &Session) -> bool {
         return false;
     }
 
-    // Single mode keeps debuginfo in the same object file, but in such a way that it it skipped
-    // by the linker - so it's expected that when codegen units are linked together that this
-    // debuginfo would be lost without keeping around the temps.
-    if sess.opts.debugging_opts.split_dwarf == config::SplitDwarfKind::Single {
-        return true;
-    }
-
-    // If we're on OSX then the equivalent of split dwarf is turned on by
-    // default. The final executable won't actually have any debug information
-    // except it'll have pointers to elsewhere. Historically we've always run
-    // `dsymutil` to "link all the dwarf together" but this is actually sort of
-    // a bummer for incremental compilation! (the whole point of split dwarf is
-    // that you don't do this sort of dwarf link).
-    //
-    // Basically as a result this just means that if we're on OSX and we're
-    // *not* running dsymutil then the object files are the only source of truth
-    // for debug information, so we must preserve them.
-    if sess.target.is_like_osx {
-        return !sess.opts.debugging_opts.run_dsymutil;
-    }
-
-    false
+    // "unpacked" split debuginfo means that we leave object files as the
+    // debuginfo is found in the original object files themselves
+    sess.split_debuginfo() == SplitDebuginfo::Unpacked
 }
 
 pub fn archive_search_paths(sess: &Session) -> Vec<PathBuf> {
diff --git a/compiler/rustc_codegen_ssa/src/back/write.rs b/compiler/rustc_codegen_ssa/src/back/write.rs
index c84b87964b8..6aef5cb535a 100644
--- a/compiler/rustc_codegen_ssa/src/back/write.rs
+++ b/compiler/rustc_codegen_ssa/src/back/write.rs
@@ -282,6 +282,20 @@ pub struct TargetMachineFactoryConfig {
     pub split_dwarf_file: Option<PathBuf>,
 }
 
+impl TargetMachineFactoryConfig {
+    pub fn new(
+        cgcx: &CodegenContext<impl WriteBackendMethods>,
+        module_name: &str,
+    ) -> TargetMachineFactoryConfig {
+        let split_dwarf_file = if cgcx.target_can_use_split_dwarf {
+            cgcx.output_filenames.split_dwarf_filename(cgcx.split_debuginfo, Some(module_name))
+        } else {
+            None
+        };
+        TargetMachineFactoryConfig { split_dwarf_file }
+    }
+}
+
 pub type TargetMachineFactoryFn<B> = Arc<
     dyn Fn(TargetMachineFactoryConfig) -> Result<<B as WriteBackendMethods>::TargetMachine, String>
         + Send
@@ -311,10 +325,11 @@ pub struct CodegenContext<B: WriteBackendMethods> {
     pub tm_factory: TargetMachineFactoryFn<B>,
     pub msvc_imps_needed: bool,
     pub is_pe_coff: bool,
+    pub target_can_use_split_dwarf: bool,
     pub target_pointer_width: u32,
     pub target_arch: String,
     pub debuginfo: config::DebugInfo,
-    pub split_dwarf_kind: config::SplitDwarfKind,
+    pub split_debuginfo: rustc_target::spec::SplitDebuginfo,
 
     // Number of cgus excluding the allocator/metadata modules
     pub total_cgus: usize,
@@ -1035,10 +1050,11 @@ fn start_executing_work<B: ExtraBackendMethods>(
         total_cgus,
         msvc_imps_needed: msvc_imps_needed(tcx),
         is_pe_coff: tcx.sess.target.is_like_windows,
+        target_can_use_split_dwarf: tcx.sess.target_can_use_split_dwarf(),
         target_pointer_width: tcx.sess.target.pointer_width,
         target_arch: tcx.sess.target.arch.clone(),
         debuginfo: tcx.sess.opts.debuginfo,
-        split_dwarf_kind: tcx.sess.opts.debugging_opts.split_dwarf,
+        split_debuginfo: tcx.sess.split_debuginfo(),
     };
 
     // This is the "main loop" of parallel work happening for parallel codegen.
diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs
index 55d521a9b5f..305ae23669b 100644
--- a/compiler/rustc_interface/src/tests.rs
+++ b/compiler/rustc_interface/src/tests.rs
@@ -17,7 +17,7 @@ use rustc_span::edition::{Edition, DEFAULT_EDITION};
 use rustc_span::symbol::sym;
 use rustc_span::SourceFileHashAlgorithm;
 use rustc_target::spec::{CodeModel, LinkerFlavor, MergeFunctions, PanicStrategy};
-use rustc_target::spec::{RelocModel, RelroLevel, TlsModel};
+use rustc_target::spec::{RelocModel, RelroLevel, SplitDebuginfo, TlsModel};
 use std::collections::{BTreeMap, BTreeSet};
 use std::iter::FromIterator;
 use std::path::PathBuf;
@@ -446,6 +446,7 @@ fn test_codegen_options_tracking_hash() {
     tracked!(profile_use, Some(PathBuf::from("abc")));
     tracked!(relocation_model, Some(RelocModel::Pic));
     tracked!(soft_float, true);
+    tracked!(split_debuginfo, Some(SplitDebuginfo::Packed));
     tracked!(target_cpu, Some(String::from("abc")));
     tracked!(target_feature, String::from("all the features, all of them"));
 }
@@ -579,7 +580,6 @@ fn test_debugging_options_tracking_hash() {
     tracked!(relax_elf_relocations, Some(true));
     tracked!(relro_level, Some(RelroLevel::Full));
     tracked!(report_delayed_bugs, true);
-    tracked!(run_dsymutil, false);
     tracked!(sanitizer, SanitizerSet::ADDRESS);
     tracked!(sanitizer_memory_track_origins, 2);
     tracked!(sanitizer_recover, SanitizerSet::ADDRESS);
diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 6e269e9e126..f9e40919149 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -13,7 +13,7 @@ use rustc_data_structures::impl_stable_hash_via_hash;
 use rustc_data_structures::stable_hasher::{HashStable, StableHasher};
 
 use rustc_target::abi::{Align, TargetDataLayout};
-use rustc_target::spec::{Target, TargetTriple};
+use rustc_target::spec::{SplitDebuginfo, Target, TargetTriple};
 
 use crate::parse::CrateConfig;
 use rustc_feature::UnstableFeatures;
@@ -221,23 +221,6 @@ pub enum DebugInfo {
     Full,
 }
 
-/// Some debuginfo requires link-time relocation and some does not. LLVM can partition the debuginfo
-/// into sections depending on whether or not it requires link-time relocation. Split DWARF
-/// provides a mechanism which allows the linker to skip the sections which don't require link-time
-/// relocation - either by putting those sections into DWARF object files, or keeping them in the
-/// object file in such a way that the linker will skip them.
-#[derive(Clone, Copy, Debug, PartialEq, Hash)]
-pub enum SplitDwarfKind {
-    /// Disabled.
-    None,
-    /// Sections which do not require relocation are written into the object file but ignored
-    /// by the linker.
-    Single,
-    /// Sections which do not require relocation are written into a DWARF object (`.dwo`) file,
-    /// which is skipped by the linker by virtue of being a different file.
-    Split,
-}
-
 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
 #[derive(Encodable, Decodable)]
 pub enum OutputType {
@@ -635,10 +618,10 @@ impl OutputFilenames {
     /// mode is being used, which is the logic that this function is intended to encapsulate.
     pub fn split_dwarf_filename(
         &self,
-        split_dwarf_kind: SplitDwarfKind,
+        split_debuginfo_kind: SplitDebuginfo,
         cgu_name: Option<&str>,
     ) -> Option<PathBuf> {
-        self.split_dwarf_path(split_dwarf_kind, cgu_name)
+        self.split_dwarf_path(split_debuginfo_kind, cgu_name)
             .map(|path| path.strip_prefix(&self.out_directory).unwrap_or(&path).to_path_buf())
     }
 
@@ -646,19 +629,19 @@ impl OutputFilenames {
     /// mode is being used, which is the logic that this function is intended to encapsulate.
     pub fn split_dwarf_path(
         &self,
-        split_dwarf_kind: SplitDwarfKind,
+        split_debuginfo_kind: SplitDebuginfo,
         cgu_name: Option<&str>,
     ) -> Option<PathBuf> {
         let obj_out = self.temp_path(OutputType::Object, cgu_name);
         let dwo_out = self.temp_path_dwo(cgu_name);
-        match split_dwarf_kind {
-            SplitDwarfKind::None => None,
+        match split_debuginfo_kind {
+            SplitDebuginfo::Off => None,
             // Single mode doesn't change how DWARF is emitted, but does add Split DWARF attributes
             // (pointing at the path which is being determined here). Use the path to the current
             // object file.
-            SplitDwarfKind::Single => Some(obj_out),
+            SplitDebuginfo::Packed => Some(obj_out),
             // Split mode emits the DWARF into a different file, use that path.
-            SplitDwarfKind::Split => Some(dwo_out),
+            SplitDebuginfo::Unpacked => Some(dwo_out),
         }
     }
 }
@@ -1910,6 +1893,15 @@ pub fn build_session_options(matches: &getopts::Matches) -> Options {
 
     let pretty = parse_pretty(matches, &debugging_opts, error_format);
 
+    if !debugging_opts.unstable_options
+        && !target_triple.triple().contains("apple")
+        && cg.split_debuginfo.is_some()
+    {
+        {
+            early_error(error_format, "`-Csplit-debuginfo` is unstable on this platform");
+        }
+    }
+
     Options {
         crate_types,
         optimize: opt_level,
@@ -2191,7 +2183,7 @@ crate mod dep_tracking {
     use rustc_feature::UnstableFeatures;
     use rustc_span::edition::Edition;
     use rustc_target::spec::{CodeModel, MergeFunctions, PanicStrategy, RelocModel};
-    use rustc_target::spec::{RelroLevel, TargetTriple, TlsModel};
+    use rustc_target::spec::{RelroLevel, SplitDebuginfo, TargetTriple, TlsModel};
     use std::collections::hash_map::DefaultHasher;
     use std::collections::BTreeMap;
     use std::hash::Hash;
@@ -2263,6 +2255,7 @@ crate mod dep_tracking {
     impl_dep_tracking_hash_via_hash!(TargetTriple);
     impl_dep_tracking_hash_via_hash!(Edition);
     impl_dep_tracking_hash_via_hash!(LinkerPluginLto);
+    impl_dep_tracking_hash_via_hash!(Option<SplitDebuginfo>);
     impl_dep_tracking_hash_via_hash!(SwitchWithOptPath);
     impl_dep_tracking_hash_via_hash!(Option<SymbolManglingVersion>);
     impl_dep_tracking_hash_via_hash!(Option<SourceFileHashAlgorithm>);
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 30af65e49a0..2aaab84585d 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -6,7 +6,7 @@ use crate::search_paths::SearchPath;
 use crate::utils::NativeLibKind;
 
 use rustc_target::spec::{CodeModel, LinkerFlavor, MergeFunctions, PanicStrategy};
-use rustc_target::spec::{RelocModel, RelroLevel, TargetTriple, TlsModel};
+use rustc_target::spec::{RelocModel, RelroLevel, SplitDebuginfo, TargetTriple, TlsModel};
 
 use rustc_feature::UnstableFeatures;
 use rustc_span::edition::Edition;
@@ -269,7 +269,6 @@ macro_rules! options {
         pub const parse_switch_with_opt_path: &str =
             "an optional path to the profiling data output directory";
         pub const parse_merge_functions: &str = "one of: `disabled`, `trampolines`, or `aliases`";
-        pub const parse_split_dwarf_kind: &str = "one of: `none`, `single` or `split`";
         pub const parse_symbol_mangling_version: &str = "either `legacy` or `v0` (RFC 2603)";
         pub const parse_src_file_hash: &str = "either `md5` or `sha1`";
         pub const parse_relocation_model: &str =
@@ -280,6 +279,8 @@ macro_rules! options {
             "one of supported TLS models (`rustc --print tls-models`)";
         pub const parse_target_feature: &str = parse_string;
         pub const parse_wasi_exec_model: &str = "either `command` or `reactor`";
+        pub const parse_split_debuginfo: &str =
+            "one of supported split-debuginfo modes (`off` or `dsymutil`)";
     }
 
     #[allow(dead_code)]
@@ -678,19 +679,6 @@ macro_rules! options {
             true
         }
 
-        fn parse_split_dwarf_kind(
-            slot: &mut SplitDwarfKind,
-            v: Option<&str>,
-        ) -> bool {
-            *slot = match v {
-                Some("none") => SplitDwarfKind::None,
-                Some("split") => SplitDwarfKind::Split,
-                Some("single") => SplitDwarfKind::Single,
-                _ => return false,
-            };
-            true
-        }
-
         fn parse_symbol_mangling_version(
             slot: &mut Option<SymbolManglingVersion>,
             v: Option<&str>,
@@ -732,6 +720,14 @@ macro_rules! options {
             }
             true
         }
+
+        fn parse_split_debuginfo(slot: &mut Option<SplitDebuginfo>, v: Option<&str>) -> bool {
+            match v.and_then(|s| SplitDebuginfo::from_str(s).ok()) {
+                Some(e) => *slot = Some(e),
+                _ => return false,
+            }
+            true
+        }
     }
 ) }
 
@@ -830,6 +826,8 @@ options! {CodegenOptions, CodegenSetter, basic_codegen_options,
         "save all temporary output files during compilation (default: no)"),
     soft_float: bool = (false, parse_bool, [TRACKED],
         "use soft float ABI (*eabihf targets only) (default: no)"),
+    split_debuginfo: Option<SplitDebuginfo> = (None, parse_split_debuginfo, [TRACKED],
+        "how to handle split-debuginfo, a platform-specific option"),
     target_cpu: Option<String> = (None, parse_opt_string, [TRACKED],
         "select target processor (`rustc --print target-cpus` for details)"),
     target_feature: String = (String::new(), parse_target_feature, [TRACKED],
@@ -1073,11 +1071,6 @@ options! {DebuggingOptions, DebuggingSetter, basic_debugging_options,
         "choose which RELRO level to use"),
     report_delayed_bugs: bool = (false, parse_bool, [TRACKED],
         "immediately print bugs registered with `delay_span_bug` (default: no)"),
-    // The default historical behavior was to always run dsymutil, so we're
-    // preserving that temporarily, but we're likely to switch the default
-    // soon.
-    run_dsymutil: bool = (true, parse_bool, [TRACKED],
-        "if on Mac, run `dsymutil` and delete intermediate object files (default: yes)"),
     sanitizer: SanitizerSet = (SanitizerSet::empty(), parse_sanitizers, [TRACKED],
         "use a sanitizer"),
     sanitizer_memory_track_origins: usize = (0, parse_sanitizer_memory_track_origins, [TRACKED],
@@ -1112,8 +1105,6 @@ options! {DebuggingOptions, DebuggingSetter, basic_debugging_options,
         "hash algorithm of source files in debug info (`md5`, `sha1`, or `sha256`)"),
     strip: Strip = (Strip::None, parse_strip, [UNTRACKED],
         "tell the linker which information to strip (`none` (default), `debuginfo` or `symbols`)"),
-    split_dwarf: SplitDwarfKind = (SplitDwarfKind::None, parse_split_dwarf_kind, [UNTRACKED],
-        "enable generation of split dwarf"),
     split_dwarf_inlining: bool = (true, parse_bool, [UNTRACKED],
         "provide minimal debug info in the object/executable to facilitate online \
          symbolication/stack traces in the absence of .dwo/.dwp files when using Split DWARF"),
diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs
index 6d018542286..dad21e59502 100644
--- a/compiler/rustc_session/src/session.rs
+++ b/compiler/rustc_session/src/session.rs
@@ -28,7 +28,7 @@ use rustc_span::source_map::{FileLoader, MultiSpan, RealFileLoader, SourceMap, S
 use rustc_span::{sym, SourceFileHashAlgorithm, Symbol};
 use rustc_target::asm::InlineAsmArch;
 use rustc_target::spec::{CodeModel, PanicStrategy, RelocModel, RelroLevel};
-use rustc_target::spec::{Target, TargetTriple, TlsModel};
+use rustc_target::spec::{SplitDebuginfo, Target, TargetTriple, TlsModel};
 
 use std::cell::{self, RefCell};
 use std::env;
@@ -804,6 +804,14 @@ impl Session {
             )
     }
 
+    pub fn split_debuginfo(&self) -> SplitDebuginfo {
+        self.opts.cg.split_debuginfo.unwrap_or(self.target.split_debuginfo)
+    }
+
+    pub fn target_can_use_split_dwarf(&self) -> bool {
+        !self.target.is_like_windows && !self.target.is_like_osx
+    }
+
     pub fn must_not_eliminate_frame_pointers(&self) -> bool {
         // "mcount" function relies on stack pointer.
         // See <https://sourceware.org/binutils/docs/gprof/Implementation.html>.
diff --git a/compiler/rustc_target/src/spec/apple_base.rs b/compiler/rustc_target/src/spec/apple_base.rs
index 88422395216..3b458962b3d 100644
--- a/compiler/rustc_target/src/spec/apple_base.rs
+++ b/compiler/rustc_target/src/spec/apple_base.rs
@@ -1,6 +1,6 @@
 use std::env;
 
-use crate::spec::{LinkArgs, TargetOptions};
+use crate::spec::{LinkArgs, SplitDebuginfo, TargetOptions};
 
 pub fn opts(os: &str) -> TargetOptions {
     // ELF TLS is only available in macOS 10.7+. If you try to compile for 10.6
@@ -36,6 +36,10 @@ pub fn opts(os: &str) -> TargetOptions {
         emit_debug_gdb_scripts: false,
         eh_frame_header: false,
 
+        // The historical default for macOS targets is to run `dsymutil` which
+        // generates a packed version of debuginfo split from the main file.
+        split_debuginfo: SplitDebuginfo::Packed,
+
         // This environment variable is pretty magical but is intended for
         // producing deterministic builds. This was first discovered to be used
         // by the `ar` tool as a way to control whether or not mtime entries in
diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs
index 90d35efaa25..0227febd294 100644
--- a/compiler/rustc_target/src/spec/mod.rs
+++ b/compiler/rustc_target/src/spec/mod.rs
@@ -448,6 +448,69 @@ impl fmt::Display for LinkOutputKind {
 
 pub type LinkArgs = BTreeMap<LinkerFlavor, Vec<String>>;
 
+#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq)]
+pub enum SplitDebuginfo {
+    /// Split debug-information is disabled, meaning that on supported platforms
+    /// you can find all debug information in the executable itself. This is
+    /// only supported for ELF effectively.
+    ///
+    /// * Windows - not supported
+    /// * macOS - don't run `dsymutil`
+    /// * ELF - `.dwarf_*` sections
+    Off,
+
+    /// Split debug-information can be found in a "packed" location separate
+    /// from the final artifact. This is supported on all platforms.
+    ///
+    /// * Windows - `*.pdb`
+    /// * macOS - `*.dSYM` (run `dsymutil`)
+    /// * ELF - `*.dwp` (run `rust-llvm-dwp`)
+    Packed,
+
+    /// Split debug-information can be found in individual object files on the
+    /// filesystem. The main executable may point to the object files.
+    ///
+    /// * Windows - not supported
+    /// * macOS - supported, scattered object files
+    /// * ELF - supported, scattered `*.dwo` files
+    Unpacked,
+}
+
+impl SplitDebuginfo {
+    fn as_str(&self) -> &'static str {
+        match self {
+            SplitDebuginfo::Off => "off",
+            SplitDebuginfo::Packed => "packed",
+            SplitDebuginfo::Unpacked => "unpacked",
+        }
+    }
+}
+
+impl FromStr for SplitDebuginfo {
+    type Err = ();
+
+    fn from_str(s: &str) -> Result<SplitDebuginfo, ()> {
+        Ok(match s {
+            "off" => SplitDebuginfo::Off,
+            "unpacked" => SplitDebuginfo::Unpacked,
+            "packed" => SplitDebuginfo::Packed,
+            _ => return Err(()),
+        })
+    }
+}
+
+impl ToJson for SplitDebuginfo {
+    fn to_json(&self) -> Json {
+        self.as_str().to_json()
+    }
+}
+
+impl fmt::Display for SplitDebuginfo {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(self.as_str())
+    }
+}
+
 macro_rules! supported_targets {
     ( $(($( $triple:literal, )+ $module:ident ),)+ ) => {
         $(mod $module;)+
@@ -1085,6 +1148,10 @@ pub struct TargetOptions {
     /// Is true if the target is an ARM architecture using thumb v1 which allows for
     /// thumb and arm interworking.
     pub has_thumb_interworking: bool,
+
+    /// How to handle split debug information, if at all. Specifying `None` has
+    /// target-specific meaning.
+    pub split_debuginfo: SplitDebuginfo,
 }
 
 impl Default for TargetOptions {
@@ -1184,6 +1251,7 @@ impl Default for TargetOptions {
             use_ctors_section: false,
             eh_frame_header: true,
             has_thumb_interworking: false,
+            split_debuginfo: SplitDebuginfo::Off,
         }
     }
 }
@@ -1382,6 +1450,18 @@ impl Target {
                     Some(Ok(()))
                 })).unwrap_or(Ok(()))
             } );
+            ($key_name:ident, SplitDebuginfo) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.find(&name[..]).and_then(|o| o.as_string().and_then(|s| {
+                    match s.parse::<SplitDebuginfo>() {
+                        Ok(level) => base.$key_name = level,
+                        _ => return Some(Err(format!("'{}' is not a valid value for \
+                                                      split-debuginfo. Use 'off' or 'dsymutil'.",
+                                                      s))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
             ($key_name:ident, list) => ( {
                 let name = (stringify!($key_name)).replace("_", "-");
                 if let Some(v) = obj.find(&name).and_then(Json::as_array) {
@@ -1627,6 +1707,7 @@ impl Target {
         key!(use_ctors_section, bool);
         key!(eh_frame_header, bool);
         key!(has_thumb_interworking, bool);
+        key!(split_debuginfo, SplitDebuginfo)?;
 
         // NB: The old name is deprecated, but support for it is retained for
         // compatibility.
@@ -1862,6 +1943,7 @@ impl ToJson for Target {
         target_option_val!(use_ctors_section);
         target_option_val!(eh_frame_header);
         target_option_val!(has_thumb_interworking);
+        target_option_val!(split_debuginfo);
 
         if default.unsupported_abis != self.unsupported_abis {
             d.insert(
diff --git a/compiler/rustc_target/src/spec/msvc_base.rs b/compiler/rustc_target/src/spec/msvc_base.rs
index 8cd6735a8c1..39c0d5f0bb4 100644
--- a/compiler/rustc_target/src/spec/msvc_base.rs
+++ b/compiler/rustc_target/src/spec/msvc_base.rs
@@ -1,4 +1,4 @@
-use crate::spec::{LinkArgs, LinkerFlavor, LldFlavor, TargetOptions};
+use crate::spec::{LinkArgs, LinkerFlavor, LldFlavor, SplitDebuginfo, TargetOptions};
 
 pub fn opts() -> TargetOptions {
     let pre_link_args_msvc = vec![
@@ -27,6 +27,10 @@ pub fn opts() -> TargetOptions {
         abi_return_struct_as_int: true,
         emit_debug_gdb_scripts: false,
 
+        // Currently this is the only supported method of debuginfo on MSVC
+        // where `*.pdb` files show up next to the final artifact.
+        split_debuginfo: SplitDebuginfo::Packed,
+
         ..Default::default()
     }
 }
diff --git a/src/bootstrap/builder.rs b/src/bootstrap/builder.rs
index 62065e27dd9..2f655e3b396 100644
--- a/src/bootstrap/builder.rs
+++ b/src/bootstrap/builder.rs
@@ -1139,10 +1139,18 @@ impl<'a> Builder<'a> {
         // itself, we skip it by default since we know it's safe to do so in that case.
         // See https://github.com/rust-lang/rust/issues/79361 for more info on this flag.
         if target.contains("apple") {
-            if self.config.rust_run_dsymutil {
-                rustflags.arg("-Zrun-dsymutil=yes");
+            if stage == 0 {
+                if self.config.rust_run_dsymutil {
+                    rustflags.arg("-Zrun-dsymutil=yes");
+                } else {
+                    rustflags.arg("-Zrun-dsymutil=no");
+                }
             } else {
-                rustflags.arg("-Zrun-dsymutil=no");
+                if self.config.rust_run_dsymutil {
+                    rustflags.arg("-Csplit-debuginfo=packed");
+                } else {
+                    rustflags.arg("-Csplit-debuginfo=unpacked");
+                }
             }
         }
 
diff --git a/src/doc/rustc/src/codegen-options/index.md b/src/doc/rustc/src/codegen-options/index.md
index f6493e49c3c..51e7d987d9d 100644
--- a/src/doc/rustc/src/codegen-options/index.md
+++ b/src/doc/rustc/src/codegen-options/index.md
@@ -492,6 +492,34 @@ point instructions in software. It takes one of the following values:
 * `y`, `yes`, `on`, or no value: use soft floats.
 * `n`, `no`, or `off`: use hardware floats (the default).
 
+## split-debuginfo
+
+This option controls the emission of "split debuginfo" for debug information
+that `rustc` generates. The default behavior of this option is
+platform-specific, and not all possible values for this option work on all
+platform. Possible values are:
+
+* `off` - This is the default for platforms with ELF binaries and windows-gnu
+  (not Windows MSVC and not macOS). This typically means that dwarf debug
+  information can be found in the final artifact in sections of the executable.
+  This option is not supported on Windows MSVC. On macOS this options prevents
+  the final execution of `dsymutil` to generate debuginfo.
+
+* `packed` - This is the default for Windows MSVC and macOS platforms. The term
+  "packed" here means that all the debug information is packed into a separate
+  file from the main executable. On Windows MSVC this is a `*.pdb` file, on
+  macOS this is a `*.dSYM` folder, and on other platforms this is a `*.dwp`
+  files.
+
+* `unpacked` - This means that debug information will be found in separate
+  files for each compilation unit (object file). This is not supported on
+  Windows MSVC. On macOS this means the original object files will contain
+  debug information. On other Unix platforms this means that `*.dwo` files will
+  contain debug information.
+
+Note that `packed` and `unpacked` gated behind `-Zunstable-options` on
+non-macOS platforms at this time.
+
 ## target-cpu
 
 This instructs `rustc` to generate code specifically for a particular processor.
@@ -499,7 +527,7 @@ This instructs `rustc` to generate code specifically for a particular processor.
 You can run `rustc --print target-cpus` to see the valid options to pass
 here. Each target has a default base CPU. Special values include:
 
-* `native` can be passed to use the processor of the host machine. 
+* `native` can be passed to use the processor of the host machine.
 * `generic` refers to an LLVM target with minimal features but modern tuning.
 
 ## target-feature
diff --git a/src/test/run-make-fulldeps/split-debuginfo/Makefile b/src/test/run-make-fulldeps/split-debuginfo/Makefile
new file mode 100644
index 00000000000..e8e62efe01c
--- /dev/null
+++ b/src/test/run-make-fulldeps/split-debuginfo/Makefile
@@ -0,0 +1,59 @@
+-include ../tools.mk
+
+all: off packed unpacked
+
+ifeq ($(UNAME),Darwin)
+# If disabled, don't run dsymutil
+off:
+	rm -rf $(TMPDIR)/*.dSYM
+	$(RUSTC) foo.rs -g -C split-debuginfo=off
+	[ ! -d $(TMPDIR)/foo.dSYM ]
+
+# Packed by default, but only if debuginfo is requested
+packed:
+	rm -rf $(TMPDIR)/*.dSYM
+	$(RUSTC) foo.rs
+	[ ! -d $(TMPDIR)/foo.dSYM ]
+	rm -rf $(TMPDIR)/*.dSYM
+	$(RUSTC) foo.rs -g
+	[ -d $(TMPDIR)/foo.dSYM ]
+	rm -rf $(TMPDIR)/*.dSYM
+	$(RUSTC) foo.rs -g -C split-debuginfo=packed
+	[ -d $(TMPDIR)/foo.dSYM ]
+	rm -rf $(TMPDIR)/*.dSYM
+
+# Object files are preserved with unpacked and `dsymutil` isn't run
+unpacked:
+	$(RUSTC) foo.rs -g -C split-debuginfo=unpacked
+	ls $(TMPDIR)/*.o
+	[ ! -d $(TMPDIR)/foo.dSYM ]
+else
+ifdef IS_WINDOWS
+# Windows only supports =off
+off:
+packed:
+unpacked:
+else
+# If disabled, don't run dsymutil
+off:
+	$(RUSTC) foo.rs -g -C split-debuginfo=off -Z unstable-options
+	[ ! -f $(TMPDIR)/*.dwp ]
+	[ ! -f $(TMPDIR)/*.dwo ]
+
+	$(RUSTC) foo.rs -g
+	[ ! -f $(TMPDIR)/*.dwp ]
+	[ ! -f $(TMPDIR)/*.dwo ]
+
+packed:
+	$(RUSTC) foo.rs -g -C split-debuginfo=packed -Z unstable-options
+	ls $(TMPDIR)/*.dwp
+	ls $(TMPDIR)/*.dwo && exit 1 || exit 0
+	rm -rf $(TMPDIR)/*.dwp
+
+unpacked:
+	$(RUSTC) foo.rs -g -C split-debuginfo=unpacked -Z unstable-options
+	ls $(TMPDIR)/*.dwp && exit 1 || exit 0
+	ls $(TMPDIR)/*.dwo
+	rm -rf $(TMPDIR)/*.dwo
+endif
+endif
diff --git a/src/test/run-make-fulldeps/split-debuginfo/foo.rs b/src/test/run-make-fulldeps/split-debuginfo/foo.rs
new file mode 100644
index 00000000000..f328e4d9d04
--- /dev/null
+++ b/src/test/run-make-fulldeps/split-debuginfo/foo.rs
@@ -0,0 +1 @@
+fn main() {}
diff --git a/src/test/run-make-fulldeps/split-dwarf/Makefile b/src/test/run-make-fulldeps/split-dwarf/Makefile
index e1a78e2edfc..93dfc8e76a9 100644
--- a/src/test/run-make-fulldeps/split-dwarf/Makefile
+++ b/src/test/run-make-fulldeps/split-dwarf/Makefile
@@ -3,6 +3,6 @@
 # only-linux
 
 all:
-	$(RUSTC) -Z split-dwarf=split foo.rs
+	$(RUSTC) -Z unstable-options -C split-debuginfo=packed foo.rs -g
 	rm $(TMPDIR)/foo.dwp
 	rm $(TMPDIR)/$(call BIN,foo)
diff --git a/src/test/ui/backtrace-apple-no-dsymutil.rs b/src/test/ui/backtrace-apple-no-dsymutil.rs
new file mode 100644
index 00000000000..492ff6356bc
--- /dev/null
+++ b/src/test/ui/backtrace-apple-no-dsymutil.rs
@@ -0,0 +1,30 @@
+// run-pass
+
+// compile-flags:-g -Csplit-debuginfo=unpacked
+// only-macos
+
+#![feature(backtrace)]
+
+use std::process::Command;
+use std::str;
+
+#[inline(never)]
+fn main() {
+    let args: Vec<String> = std::env::args().collect();
+    if args.len() >= 2 {
+        println!("{}", std::backtrace::Backtrace::force_capture());
+        return;
+    }
+    let out = Command::new(&args[0]).env("RUST_BACKTRACE", "1").arg("foo").output().unwrap();
+    let output = format!(
+        "{}\n{}",
+        str::from_utf8(&out.stdout).unwrap(),
+        str::from_utf8(&out.stderr).unwrap(),
+    );
+    if out.status.success() && output.contains(file!()) {
+        return;
+    }
+    println!("status: {}", out.status);
+    println!("child output:\n\t{}", output.replace("\n", "\n\t"));
+    panic!("failed to find {:?} in output", file!());
+}
diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs
index 5608ff98417..52aed57fc76 100644
--- a/src/tools/compiletest/src/runtest.rs
+++ b/src/tools/compiletest/src/runtest.rs
@@ -2015,10 +2015,10 @@ impl<'test> TestCx<'test> {
                 rustc.args(&["-Zchalk"]);
             }
             Some(CompareMode::SplitDwarf) => {
-                rustc.args(&["-Zsplit-dwarf=split"]);
+                rustc.args(&["-Csplit-debuginfo=unpacked", "-Zunstable-options"]);
             }
             Some(CompareMode::SplitDwarfSingle) => {
-                rustc.args(&["-Zsplit-dwarf=single"]);
+                rustc.args(&["-Csplit-debuginfo=packed", "-Zunstable-options"]);
             }
             None => {}
         }
diff --git a/src/tools/tidy/src/ui_tests.rs b/src/tools/tidy/src/ui_tests.rs
index e687901f212..21d05226fb4 100644
--- a/src/tools/tidy/src/ui_tests.rs
+++ b/src/tools/tidy/src/ui_tests.rs
@@ -7,7 +7,7 @@ use std::path::Path;
 
 const ENTRY_LIMIT: usize = 1000;
 // FIXME: The following limits should be reduced eventually.
-const ROOT_ENTRY_LIMIT: usize = 1458;
+const ROOT_ENTRY_LIMIT: usize = 1459;
 const ISSUES_ENTRY_LIMIT: usize = 2669;
 
 fn check_entries(path: &Path, bad: &mut bool) {