diff --git a/compiler/rustc_codegen_llvm/messages.ftl b/compiler/rustc_codegen_llvm/messages.ftl
index e5df417370bb9..864d82cf9ba72 100644
--- a/compiler/rustc_codegen_llvm/messages.ftl
+++ b/compiler/rustc_codegen_llvm/messages.ftl
@@ -24,10 +24,10 @@ codegen_llvm_error_writing_def_file =
     Error writing .DEF file: {$error}
 
 codegen_llvm_error_calling_dlltool =
-    Error calling dlltool: {$error}
+    Error calling dlltool '{$dlltool_path}': {$error}
 
 codegen_llvm_dlltool_fail_import_library =
-    Dlltool could not create import library: {$stdout}\n{$stderr}
+    Dlltool could not create import library: {$error}
 
 codegen_llvm_target_feature_disable_or_enable =
     the target features {$features} must all be either enabled or disabled together
diff --git a/compiler/rustc_codegen_llvm/src/back/archive.rs b/compiler/rustc_codegen_llvm/src/back/archive.rs
index a570f2af0f0e5..b15a8b34b3049 100644
--- a/compiler/rustc_codegen_llvm/src/back/archive.rs
+++ b/compiler/rustc_codegen_llvm/src/back/archive.rs
@@ -198,7 +198,7 @@ impl ArchiveBuilderBuilder for LlvmArchiveBuilderBuilder {
                 "arm" => ("arm", "--32"),
                 _ => panic!("unsupported arch {}", sess.target.arch),
             };
-            let result = std::process::Command::new(dlltool)
+            let result = std::process::Command::new(&dlltool)
                 .args([
                     "-d",
                     def_file_path.to_str().unwrap(),
@@ -218,12 +218,15 @@ impl ArchiveBuilderBuilder for LlvmArchiveBuilderBuilder {
 
             match result {
                 Err(e) => {
-                    sess.emit_fatal(ErrorCallingDllTool { error: e });
+                    sess.emit_fatal(ErrorCallingDllTool {
+                        dlltool_path: dlltool.to_string_lossy(),
+                        error: e,
+                    });
                 }
-                Ok(output) if !output.status.success() => {
+                // dlltool returns '0' on failure, so check for error output instead.
+                Ok(output) if !output.stderr.is_empty() => {
                     sess.emit_fatal(DlltoolFailImportLibrary {
-                        stdout: String::from_utf8_lossy(&output.stdout),
-                        stderr: String::from_utf8_lossy(&output.stderr),
+                        error: String::from_utf8_lossy(&output.stderr),
                     })
                 }
                 _ => {}
@@ -431,7 +434,7 @@ fn string_to_io_error(s: String) -> io::Error {
 
 fn find_binutils_dlltool(sess: &Session) -> OsString {
     assert!(sess.target.options.is_like_windows && !sess.target.options.is_like_msvc);
-    if let Some(dlltool_path) = &sess.opts.unstable_opts.dlltool {
+    if let Some(dlltool_path) = &sess.opts.cg.dlltool {
         return dlltool_path.clone().into_os_string();
     }
 
diff --git a/compiler/rustc_codegen_llvm/src/errors.rs b/compiler/rustc_codegen_llvm/src/errors.rs
index bae88d942934c..0bba6ed0f2ca7 100644
--- a/compiler/rustc_codegen_llvm/src/errors.rs
+++ b/compiler/rustc_codegen_llvm/src/errors.rs
@@ -67,15 +67,15 @@ pub(crate) struct ErrorWritingDEFFile {
 
 #[derive(Diagnostic)]
 #[diag(codegen_llvm_error_calling_dlltool)]
-pub(crate) struct ErrorCallingDllTool {
+pub(crate) struct ErrorCallingDllTool<'a> {
+    pub dlltool_path: Cow<'a, str>,
     pub error: std::io::Error,
 }
 
 #[derive(Diagnostic)]
 #[diag(codegen_llvm_dlltool_fail_import_library)]
 pub(crate) struct DlltoolFailImportLibrary<'a> {
-    pub stdout: Cow<'a, str>,
-    pub stderr: Cow<'a, str>,
+    pub error: Cow<'a, str>,
 }
 
 #[derive(Diagnostic)]
diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs
index 014810dba9cce..7a4b690018aff 100644
--- a/compiler/rustc_interface/src/tests.rs
+++ b/compiler/rustc_interface/src/tests.rs
@@ -545,6 +545,7 @@ fn test_codegen_options_tracking_hash() {
     untracked!(ar, String::from("abc"));
     untracked!(codegen_units, Some(42));
     untracked!(default_linker_libraries, true);
+    untracked!(dlltool, Some(PathBuf::from("custom_dlltool.exe")));
     untracked!(extra_filename, String::from("extra-filename"));
     untracked!(incremental, Some(String::from("abc")));
     // `link_arg` is omitted because it just forwards to `link_args`.
@@ -649,7 +650,6 @@ fn test_unstable_options_tracking_hash() {
     untracked!(assert_incr_state, Some(String::from("loaded")));
     untracked!(deduplicate_diagnostics, false);
     untracked!(dep_tasks, true);
-    untracked!(dlltool, Some(PathBuf::from("custom_dlltool.exe")));
     untracked!(dont_buffer_diagnostics, true);
     untracked!(dump_dep_graph, true);
     untracked!(dump_drop_tracking_cfg, Some("cfg.dot".to_string()));
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 0548379dc2fc1..ae7082c875840 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -1205,6 +1205,8 @@ options! {
         2 = full debug info with variable and type information; default: 0)"),
     default_linker_libraries: bool = (false, parse_bool, [UNTRACKED],
         "allow the linker to link its default libraries (default: no)"),
+    dlltool: Option<PathBuf> = (None, parse_opt_pathbuf, [UNTRACKED],
+        "import library generation tool (windows-gnu only)"),
     embed_bitcode: bool = (true, parse_bool, [TRACKED],
         "emit bitcode in rlibs (default: yes)"),
     extra_filename: String = (String::new(), parse_string, [UNTRACKED],
@@ -1361,8 +1363,6 @@ options! {
         (default: no)"),
     diagnostic_width: Option<usize> = (None, parse_opt_number, [UNTRACKED],
         "set the current output width for diagnostic truncation"),
-    dlltool: Option<PathBuf> = (None, parse_opt_pathbuf, [UNTRACKED],
-        "import library generation tool (windows-gnu only)"),
     dont_buffer_diagnostics: bool = (false, parse_bool, [UNTRACKED],
         "emit diagnostics rather than buffering (breaks NLL error downgrading, sorting) \
         (default: no)"),
diff --git a/src/doc/rustc/src/codegen-options/index.md b/src/doc/rustc/src/codegen-options/index.md
index c7f120dafeafd..bc106f63efc37 100644
--- a/src/doc/rustc/src/codegen-options/index.md
+++ b/src/doc/rustc/src/codegen-options/index.md
@@ -88,6 +88,14 @@ It takes one of the following values:
 For example, for gcc flavor linkers, this issues the `-nodefaultlibs` flag to
 the linker.
 
+## dlltool
+
+On `windows-gnu` targets, this flag controls which dlltool `rustc` invokes to
+generate import libraries when using the [`raw-dylib` link kind](../../reference/items/external-blocks.md#the-link-attribute).
+It takes a path to [the dlltool executable](https://sourceware.org/binutils/docs/binutils/dlltool.html).
+If this flag is not specified, a dlltool executable will be inferred based on
+the host environment and target.
+
 ## embed-bitcode
 
 This flag controls whether or not the compiler embeds LLVM bitcode into object
diff --git a/src/tools/compiletest/src/header.rs b/src/tools/compiletest/src/header.rs
index 5bc9d9afcb9d1..5753de1f37b24 100644
--- a/src/tools/compiletest/src/header.rs
+++ b/src/tools/compiletest/src/header.rs
@@ -976,6 +976,15 @@ pub fn make_test_description<R: Read>(
     #[cfg(not(windows))]
     let (has_i686_dlltool, has_x86_64_dlltool) =
         (is_on_path("i686-w64-mingw32-dlltool"), is_on_path("x86_64-w64-mingw32-dlltool"));
+    let has_dlltool = || {
+        if config.matches_arch("x86") {
+            has_i686_dlltool()
+        } else if config.matches_arch("x86_64") {
+            has_x86_64_dlltool()
+        } else {
+            false
+        }
+    };
 
     iter_header(path, src, &mut |revision, ln| {
         if revision.is_some() && revision != cfg {
@@ -1046,6 +1055,7 @@ pub fn make_test_description<R: Read>(
         reason!(!has_rust_lld && config.parse_name_directive(ln, "needs-rust-lld"));
         reason!(config.parse_name_directive(ln, "needs-i686-dlltool") && !has_i686_dlltool());
         reason!(config.parse_name_directive(ln, "needs-x86_64-dlltool") && !has_x86_64_dlltool());
+        reason!(config.parse_name_directive(ln, "needs-dlltool") && !has_dlltool());
         should_fail |= config.parse_name_directive(ln, "should-fail");
     });
 
diff --git a/tests/run-make/raw-dylib-custom-dlltool/Makefile b/tests/run-make/raw-dylib-custom-dlltool/Makefile
new file mode 100644
index 0000000000000..ce0091dceb837
--- /dev/null
+++ b/tests/run-make/raw-dylib-custom-dlltool/Makefile
@@ -0,0 +1,11 @@
+# Test using -Cdlltool to change where raw-dylib looks for the dlltool binary.
+
+# only-windows
+# only-gnu
+# needs-dlltool
+
+include ../../run-make-fulldeps/tools.mk
+
+all:
+	$(RUSTC) --crate-type lib --crate-name raw_dylib_test lib.rs -Cdlltool=$(CURDIR)/script.cmd
+	$(DIFF) output.txt "$(TMPDIR)"/output.txt
diff --git a/tests/run-make/raw-dylib-custom-dlltool/lib.rs b/tests/run-make/raw-dylib-custom-dlltool/lib.rs
new file mode 100644
index 0000000000000..473c765966caf
--- /dev/null
+++ b/tests/run-make/raw-dylib-custom-dlltool/lib.rs
@@ -0,0 +1,12 @@
+#![feature(raw_dylib)]
+
+#[link(name = "extern_1", kind = "raw-dylib")]
+extern {
+    fn extern_fn_1();
+}
+
+pub fn library_function() {
+    unsafe {
+        extern_fn_1();
+    }
+}
diff --git a/tests/run-make/raw-dylib-custom-dlltool/output.txt b/tests/run-make/raw-dylib-custom-dlltool/output.txt
new file mode 100644
index 0000000000000..6dd9466d26ddc
--- /dev/null
+++ b/tests/run-make/raw-dylib-custom-dlltool/output.txt
@@ -0,0 +1 @@
+Called dlltool via script.cmd
diff --git a/tests/run-make/raw-dylib-custom-dlltool/script.cmd b/tests/run-make/raw-dylib-custom-dlltool/script.cmd
new file mode 100644
index 0000000000000..95f85c61c67d2
--- /dev/null
+++ b/tests/run-make/raw-dylib-custom-dlltool/script.cmd
@@ -0,0 +1,2 @@
+echo Called dlltool via script.cmd> %TMPDIR%\output.txt
+dlltool.exe %*
diff --git a/tests/rustdoc-ui/c-help.stdout b/tests/rustdoc-ui/c-help.stdout
index 75b2e2a2a43f4..d05a6b8406b01 100644
--- a/tests/rustdoc-ui/c-help.stdout
+++ b/tests/rustdoc-ui/c-help.stdout
@@ -5,6 +5,7 @@
     -C         debug-assertions=val -- explicitly enable the `cfg(debug_assertions)` directive
     -C                debuginfo=val -- debug info emission level (0 = no debug info, 1 = line tables only, 2 = full debug info with variable and type information; default: 0)
     -C default-linker-libraries=val -- allow the linker to link its default libraries (default: no)
+    -C                  dlltool=val -- import library generation tool (windows-gnu only)
     -C            embed-bitcode=val -- emit bitcode in rlibs (default: yes)
     -C           extra-filename=val -- extra data to put in each output filename
     -C     force-frame-pointers=val -- force use of the frame pointers
diff --git a/tests/rustdoc-ui/z-help.stdout b/tests/rustdoc-ui/z-help.stdout
index 5ad38e4fd9821..6a9303f9a577e 100644
--- a/tests/rustdoc-ui/z-help.stdout
+++ b/tests/rustdoc-ui/z-help.stdout
@@ -17,7 +17,6 @@
     -Z                dep-info-omit-d-target=val -- in dep-info output, omit targets for tracking dependencies of the dep-info files themselves (default: no)
     -Z                             dep-tasks=val -- print tasks that execute and the color their dep node gets (requires debug build) (default: no)
     -Z                      diagnostic-width=val -- set the current output width for diagnostic truncation
-    -Z                               dlltool=val -- import library generation tool (windows-gnu only)
     -Z               dont-buffer-diagnostics=val -- emit diagnostics rather than buffering (breaks NLL error downgrading, sorting) (default: no)
     -Z                         drop-tracking=val -- enables drop tracking in generators (default: no)
     -Z                     drop-tracking-mir=val -- enables drop tracking on MIR in generators (default: no)
diff --git a/tests/ui/rfc-2627-raw-dylib/dlltool-failed.rs b/tests/ui/rfc-2627-raw-dylib/dlltool-failed.rs
new file mode 100644
index 0000000000000..9378bd473c582
--- /dev/null
+++ b/tests/ui/rfc-2627-raw-dylib/dlltool-failed.rs
@@ -0,0 +1,20 @@
+// Tests that dlltool failing to generate an import library will raise an error.
+
+// only-gnu
+// only-windows
+// needs-dlltool
+// compile-flags: --crate-type lib --emit link
+// normalize-stderr-test: "[^ ']*/dlltool.exe" -> "$$DLLTOOL"
+// normalize-stderr-test: "[^ ]*/foo.def" -> "$$DEF_FILE"
+#![feature(raw_dylib)]
+#[link(name = "foo", kind = "raw-dylib")]
+extern "C" {
+    // `@1` is an invalid name to export, as it usually indicates that something
+    // is being exported via ordinal.
+    #[link_name = "@1"]
+    fn f(x: i32);
+}
+
+pub fn lib_main() {
+    unsafe { f(42); }
+}
diff --git a/tests/ui/rfc-2627-raw-dylib/dlltool-failed.stderr b/tests/ui/rfc-2627-raw-dylib/dlltool-failed.stderr
new file mode 100644
index 0000000000000..68e7cc11f6a72
--- /dev/null
+++ b/tests/ui/rfc-2627-raw-dylib/dlltool-failed.stderr
@@ -0,0 +1,4 @@
+error: Dlltool could not create import library: $DLLTOOL: Syntax error in def file $DEF_FILE:1
+
+error: aborting due to previous error
+
diff --git a/tests/ui/rfc-2627-raw-dylib/invalid-dlltool.rs b/tests/ui/rfc-2627-raw-dylib/invalid-dlltool.rs
new file mode 100644
index 0000000000000..2b2ce5da3d0f6
--- /dev/null
+++ b/tests/ui/rfc-2627-raw-dylib/invalid-dlltool.rs
@@ -0,0 +1,14 @@
+// Tests that failing to run dlltool will raise an error.
+
+// only-gnu
+// only-windows
+// compile-flags: --crate-type lib --emit link -Cdlltool=does_not_exit.exe
+#![feature(raw_dylib)]
+#[link(name = "foo", kind = "raw-dylib")]
+extern "C" {
+    fn f(x: i32);
+}
+
+pub fn lib_main() {
+    unsafe { f(42); }
+}
diff --git a/tests/ui/rfc-2627-raw-dylib/invalid-dlltool.stderr b/tests/ui/rfc-2627-raw-dylib/invalid-dlltool.stderr
new file mode 100644
index 0000000000000..3ae901e0dbc94
--- /dev/null
+++ b/tests/ui/rfc-2627-raw-dylib/invalid-dlltool.stderr
@@ -0,0 +1,4 @@
+error: Error calling dlltool 'does_not_exit.exe': program not found
+
+error: aborting due to previous error
+