diff --git a/src/tools/run-make-support/src/external_deps/rustc.rs b/src/tools/run-make-support/src/external_deps/rustc.rs
index cece58d29566c..0c987222a4e7b 100644
--- a/src/tools/run-make-support/src/external_deps/rustc.rs
+++ b/src/tools/run-make-support/src/external_deps/rustc.rs
@@ -194,6 +194,11 @@ impl Rustc {
         self
     }
 
+    /// Make `rustc` prefere dynamic linking
+    pub fn prefer_dynamic(&mut self) -> &mut Self {
+        self.arg("-Cprefer-dynamic")
+    }
+
     /// Specify directory path used for profile generation
     pub fn profile_generate<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
         let mut arg = OsString::new();
diff --git a/src/tools/run-make-support/src/symbols.rs b/src/tools/run-make-support/src/symbols.rs
index fd0c866bcc927..54d84302df233 100644
--- a/src/tools/run-make-support/src/symbols.rs
+++ b/src/tools/run-make-support/src/symbols.rs
@@ -1,6 +1,6 @@
 use std::path::Path;
 
-use object::{self, Object, ObjectSymbol, SymbolIterator};
+use object::{self, Object, ObjectSymbol, Symbol, SymbolIterator};
 
 /// Iterate through the symbols in an object file.
 ///
@@ -42,3 +42,46 @@ pub fn any_symbol_contains(path: impl AsRef<Path>, substrings: &[&str]) -> bool
         false
     })
 }
+
+/// Get a list of symbols that are in `symbol_names` but not the final binary.
+///
+/// The symbols must also match `pred`.
+///
+/// The symbol names must match exactly.
+///
+/// Panics if `path` is not a valid object file readable by the current user.
+pub fn missing_exact_symbols<'a>(
+    path: impl AsRef<Path>,
+    symbol_names: &[&'a str],
+    pred: impl Fn(&Symbol<'_, '_>) -> bool,
+) -> Vec<&'a str> {
+    let mut found = vec![false; symbol_names.len()];
+    with_symbol_iter(path, |syms| {
+        for sym in syms.filter(&pred) {
+            for (i, symbol_name) in symbol_names.iter().enumerate() {
+                found[i] |= sym.name_bytes().unwrap() == symbol_name.as_bytes();
+            }
+        }
+    });
+    return found
+        .iter()
+        .enumerate()
+        .filter_map(|(i, found)| if !*found { Some(symbol_names[i]) } else { None })
+        .collect();
+}
+
+/// Assert that the symbol file contains all of the listed symbols and they all match the given predicate
+pub fn assert_contains_exact_symbols(
+    path: impl AsRef<Path>,
+    symbol_names: &[&str],
+    pred: impl Fn(&Symbol<'_, '_>) -> bool,
+) {
+    let missing = missing_exact_symbols(path.as_ref(), symbol_names, pred);
+    if missing.len() > 0 {
+        eprintln!("{} does not contain symbol(s): ", path.as_ref().display());
+        for sn in missing {
+            eprintln!("* {}", sn);
+        }
+        panic!("missing symbols");
+    }
+}
diff --git a/src/tools/tidy/src/allowed_run_make_makefiles.txt b/src/tools/tidy/src/allowed_run_make_makefiles.txt
index a2cfdea712e7c..8399c754d9b74 100644
--- a/src/tools/tidy/src/allowed_run_make_makefiles.txt
+++ b/src/tools/tidy/src/allowed_run_make_makefiles.txt
@@ -5,7 +5,6 @@ run-make/dep-info-doesnt-run-much/Makefile
 run-make/dep-info-spaces/Makefile
 run-make/dep-info/Makefile
 run-make/emit-to-stdout/Makefile
-run-make/extern-fn-reachable/Makefile
 run-make/incr-add-rust-src-component/Makefile
 run-make/issue-84395-lto-embed-bitcode/Makefile
 run-make/jobserver-error/Makefile
diff --git a/tests/run-make/extern-fn-reachable/Makefile b/tests/run-make/extern-fn-reachable/Makefile
deleted file mode 100644
index 3297251bfd1aa..0000000000000
--- a/tests/run-make/extern-fn-reachable/Makefile
+++ /dev/null
@@ -1,26 +0,0 @@
-# ignore-cross-compile
-include ../tools.mk
-
-# ignore-windows-msvc
-
-NM=nm -D
-
-ifeq ($(UNAME),Darwin)
-NM=nm -gU
-endif
-
-ifdef IS_WINDOWS
-NM=nm -g
-endif
-
-# This overrides the LD_LIBRARY_PATH for RUN
-TARGET_RPATH_DIR:=$(TARGET_RPATH_DIR):$(TMPDIR)
-
-all:
-	$(RUSTC) dylib.rs -o $(TMPDIR)/libdylib.so -C prefer-dynamic
-
-	[ "$$($(NM) $(TMPDIR)/libdylib.so | grep -v __imp_ | grep -c fun1)" -eq "1" ]
-	[ "$$($(NM) $(TMPDIR)/libdylib.so | grep -v __imp_ | grep -c fun2)" -eq "1" ]
-	[ "$$($(NM) $(TMPDIR)/libdylib.so | grep -v __imp_ | grep -c fun3)" -eq "1" ]
-	[ "$$($(NM) $(TMPDIR)/libdylib.so | grep -v __imp_ | grep -c fun4)" -eq "1" ]
-	[ "$$($(NM) $(TMPDIR)/libdylib.so | grep -v __imp_ | grep -c fun5)" -eq "1" ]
diff --git a/tests/run-make/extern-fn-reachable/rmake.rs b/tests/run-make/extern-fn-reachable/rmake.rs
new file mode 100644
index 0000000000000..dce2e3d526830
--- /dev/null
+++ b/tests/run-make/extern-fn-reachable/rmake.rs
@@ -0,0 +1,10 @@
+//@ ignore-cross-compile
+use run_make_support::object::ObjectSymbol;
+use run_make_support::rustc;
+use run_make_support::symbols::assert_contains_exact_symbols;
+fn main() {
+    rustc().input("dylib.rs").output("dylib.so").prefer_dynamic().run();
+    assert_contains_exact_symbols("dylib.so", &["fun1", "fun2", "fun3", "fun4", "fun5"], |sym| {
+        dbg!(dbg!(sym).is_global()) && !dbg!(sym.is_undefined())
+    });
+}