涉及问题:程序如何与操作系统(OS)交互?
核心思路:一个程序(无论用什么语言写的)通过向上级领导——“操作系统“”请求”来“打开”另一个程序(比如计算器)。“请帮我运行一个叫做calc.exe
(Windows) /gnome-calculator
(Linux) /Calculator.app
(macOS) 的程序”
完成任务:如何在特定的编程语言中,去执行一个操作系统命令?——在不同语言的“工具箱”(标准库)里,找到那个负责“与操作系统沟通、执行命令”的工具
python
python 是“胶水语言”,它能轻松地将其他程序和组件“粘合”在一起,这也意味着它和操作系统交互的功能非常方便和直接。所以python用的工具箱是什么?os库。然后在找找os库的在线文档,看看哪个长的最像是“执行一个系统指令”的。我预先猜测可以是 system
、run
、execute
,还真让我找到了是 os.system()
。这玩意打开计算器需要什么参数?更直接点,你在
里面输入的字符串calc
打开计算器的,这个就是要的参数
# 需要一个能和操作系统交互的工具箱,所以先引入一个工具箱(标准库),os
# import os
import subprocess
# 在windows上打开计算器,命令calc
command = 'calc'
# 我需要工具箱里面的工具执行命令
# os.system(command)
subprocess.run(command)
print("计算器已打开")
推荐标准库subprocess
,此库以更精细地控制子进程。
Java
Java 流氓,“一次编写,到处运行”,它通过 Java 虚拟机(JVM)来隔离底层操作系统的差异。再俗一点,把 JVM 想象成一个为 Java 程序量身打造的、与世隔绝的“VIP 包间”。无论外面的世界是 Windows、Linux 还是 macOS,只要安装了对应的 JVM,Java 程序就能在这个标准化的包间里舒适地运行,不用关心外面操作系统的风风雨雨。
那么,Java 程序要怎么透过 JVM 去和特定的操作系统打交道呢?它要如何从“VIP 包间”里探出头来,跟外面的“大堂经理”(操作系统)点单呢?
JVM 为此提供了一个“运行时环境”的抽象。在 Java 里,这个“运行时环境”被抽象为Runtime
类。然后就需要知道(获取)当前程序的 Runtime
对象是啥玩意,即是当前java程序所处的“运行时环境”,顺带看看这个对象提供了什么工具,能帮我们向操作系统“传话”。
涉及到java的反射机制,通常表现为get…(…)。俗一点,这涉及到 Java 的一个经典设计模式——单例模式。对于
Runtime
这种全局唯一的资源,Java 不让你new
一个新的,而是通过一个静态方法Runtime.getRuntime()
来获取那个已经存在的、唯一的实例。这感觉就像在问:“嘿,Java,把我们现在这个环境的管家叫来一下。”
拿到了 Runtime
对象这个管家后,找找它的工具手册里有没有听起来像是“执行”(execute)一个外部命令,exec()
。exec()方法需要一个字符串参数,而我要用它来打开计算器,于是我填入参数calc
。
// 引入处理输入输出错误的工具,因为执行外部命令可能会失败
import java.io.IOException;
public class OpenCalculator {
public static void main(String[] args) {
System.out.println("我是一个Java程序,正准备通过JVM和操作系统聊聊...");
try {
// 1. 获取当前Java程序的运行时环境对象
Runtime rt = Runtime.getRuntime();
// 2. 告诉运行时环境,请它帮忙执行一个外部命令 "calc"
// exec() 方法会返回一个 Process 对象,代表新创建的子进程(计算器)
Process p = rt.exec("calc");
System.out.println("已经把“打开计算器”的请求发出去了,操作系统应该收到了。");
} catch (IOException e) {
// 如果命令执行失败(比如找不到calc程序),就会抛出异常
System.out.println("哎呀,出错了!");
e.printStackTrace();
}
}
}
os.system()
在 Python 里如果失败了,可能只是返回一个非零值,程序继续往下跑。而 Java 的exec()
如果失败,会抛出一个IOException
,它强制你必须用try...catch
来处理这种可能的“意外情况”。这就像是你在递交请求时,必须同时准备一个“如果请求被拒该怎么办”的预案。
与 Python 类似,Java 后来也提供了一个更强大、更灵活的工具 ProcessBuilder
。它能让你更精细地配置要执行的命令、工作目录、环境变量等,是对 Runtime.exec()
的现代化升级,思想上和 Python 的 subprocess
库非常接近。
C/C++
C/C++ 作为系统级语言,与操作系统对话就像用「方言」直接喊话。它们的「杀手锏」藏在标准库 <stdlib.h>
中——system()
函数。怎么用呢?直接上代码:
#include <stdlib.h>
int main() {
system("calc"); // 像在cmd里直接喊话
return 0;
}
批处理
如果说 C 是操作系统的“母语”,那批处理(.bat 文件)就是 Windows 命令行(CMD)的“方言”。它本身就不是一个独立的程序,而是写给命令行解释器的一系列指令清单
。
所以,它与操作系统的交互模式发生了根本变化:
- 其他语言:“你好操作系统,请你帮我运行一下
calc
这个命令。” - 批处理:“我就是命令本身!下一条,
calc
!”
它不需要“请求”,它本身就在“执行”。因此,在批处理脚本里打开计算器,就是最原始、最直接的方式。
chcp 65001 > nul
@echo off
rem 这是一行注释,@echo off 的意思是不要在屏幕上显示我执行的每一条命令
echo 马上就要打开计算器了,请注意!
rem 直接写下程序名即可,就像你在CMD里输入的一样
calc
rem 为了能看到上面的提示,在这里暂停一下
pause
让脚本不等待计算器关闭就继续往下走,可以使用 start
命令:
chcp 65001 > nul
@echo off
echo 我会打开计算器,但不会等它,我会继续做我自己的事。
start calc
echo 看,执行到这里了,计算器是不是在另一个窗口打开了?
pause
不需要“工具箱”,因为它自己就是“命令行”本人
go
Go 语言是 Google 设计的一门现代化、注重工程效率的语言。它被设计用来编写高性能的系统软件,所以和操作系统打交道也是它的看家本领之一。
Go 的设计哲学是“显式”和“清晰”。它提供了一个专门的工具箱来处理这种事,名字也起得非常直白:os/exec
包。os
代表操作系统,exec
代表执行。
它的思路和 Python 的 subprocess
有点像,分为两步:
- 准备命令:告诉 Go 你想执行什么命令和参数。
- 执行命令:让准备好的命令真正运行起来。
这种方式比 C 的 system()
更安全、更可控。
// main.go 文件内容
// 告诉 Go 编译器,这个文件属于 "main" 包。
// "main" 包是一个特殊的包,它告诉 Go 这是一个可执行程序的入口,而不是一个库。
package main
// 引入我们需要的两个“工具箱”
import (
"log" // 一个简单的日志打印工具,比 fmt.Println 更适合输出程序状态信息。
"os/exec" // 这就是go中与操作系统沟通、执行命令的核心工具箱。
)
// "main" 函数是程序的起点,Go 程序会从这里开始执行。
func main() {
// 使用 exec.Command 函数来“准备”一个命令。
// 这行代码本身并不会执行任何东西,它只是创建了一个代表“calc”命令的结构体。
// 在 Windows 上是 "calc",在 macOS 上是 "open", "-a", "Calculator",在 Linux 上是 "gnome-calculator"
cmd := exec.Command("calc")
// 梦回小时候学习pascal了
// 通过 log 工具打印一条信息,告诉我们程序进展到哪一步了。
log.Println("Go程序准备就绪,即将请求操作系统打开计算器...")
// 这是真正执行命令的一步!
// cmd.Run() 会启动上面准备好的命令,并等待它执行完毕。
// 它会返回一个 error 类型的值。在 Go 中,如果一个操作可能失败,通常会返回一个 error。
err := cmd.Run()
// 这是 Go 语言经典的错误处理方式。
// 如果 err 不是 nil,就意味着在执行命令的过程中出了问题。
if err != nil {
// log.Fatalf 会打印错误信息,然后立刻终止程序。
// %v 是一个占位符,会把 err 变量的内容格式化成字符串放进去。
log.Fatalf("糟糕!命令执行失败了: %v", err)
}
// 如果程序能走到这一行,说明 cmd.Run() 没有返回错误,命令已成功发出。
log.Println("请求已发送!请检查你的桌面,计算器应该已经打开了。")
}
汇编
“硬核”部分来袭。如果说 C 是操作系统的“母语”,那汇编语言就是 CPU 能听懂的“机器指令”的助记符。这是最最底层的对话方式。
用汇编语言去打开计算器,不再是“请求”操作系统,而是直接**调用操作系统的“系统服务例程” (System Call)**。这就像是直接推开CEO办公室的门,使用他提供给核心人员的内部接口。
这个过程高度依赖于具体的操作系统和CPU架构。Windows 和 Linux 的方式完全不同。我们以 32 位 Windows 为例,它提供了一个非常古老但简单的 API 函数叫做 WinExec
,我们可以用汇编直接调用它。
这已经不是简单的写代码了,你需要理解程序的内存布局、函数调用约定等底层知识。
我使用了一个windows 2003虚拟机,我现在要做的,就是扮演编译器和链接器的角色,手动将“图纸”(.asm
源代码)打造成一个可以被 Windows 使用的“建筑”(.exe
文件)。而建东西是需要工具辅助的,用包含汇编器(ml.exe
)、链接器(link.exe
)以及代码中引用的所有 Windows 库和头文件的“工具箱”——MASM32 SDK,我认为是不错的选择。
MASM32 SDK 的官方网站:http://www.masm32.com/
我默认c盘
创建一个后缀为.asm的文件,这就是图纸
; opencalc.asm
; Intel 80386 处理器
; 32位保护的线性地址空间模型,被调函数规则stdcall,参数从右往左压栈
; casemap :none 使区分大小写
.386
.model flat, stdcall
option casemap :none
; 引入 Windows API 的定义库,这些都位于 C:\masm32\include
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
; 告诉链接器需要链接哪个库文件,这个位于 C:\masm32\lib
includelib \masm32\lib\kernel32.lib
.data
; 在数据段定义一个以0结尾的字符串,这是我们要执行的命令
szCmdLine db "calc.exe", 0
.code
start:
; 调用 WinExec 函数。
; 第一个参数: 要执行的命令字符串的地址 (addr szCmdLine)
; 第二个参数: 窗口的显示方式 (SW_SHOWNORMAL 表示正常显示)
invoke WinExec, addr szCmdLine, SW_SHOWNORMAL
; 调用 ExitProcess 正常退出程序,参数0表示没有错误
invoke ExitProcess, 0
end start
编译和链接——从“蓝图”到“可执行文件”,
- 步骤 3.1: 汇编 (Assemble)
这一步是将人类可读的opencalc.asm
文件,翻译成机器可读但还不能独立运行的“目标文件”(.obj
)。
在命令提示符里,输入以下命令并按回车:
C:\masm32\bin\ml.exe /c /Zd /coff opencalc.asm
C:\masm32\bin\ml.exe
: 这是 MASM32 的汇编器程序。
/c
: 参数告诉它“只汇编,不要链接”。
/Zd
: 添加调试信息。
/coff
: 指定生成的目标文件格式为 COFF,这是 Windows 的标准格式。
opencalc.asm
: 输入源文件。
执行后,如果没有报错,会生成一个新文件:opencalc.obj
。
2. 步骤 3.2: 链接 (Link)
这一步是将目标文件 opencalc.obj
和它所依赖的系统库(kernel32.lib
)“链接”在一起,最终生成一个完整的、可以执行的 .exe
文件。
C:\masm32\bin\link.exe /SUBSYSTEM:WINDOWS /LIBPATH:C:\masm32\lib opencalc.obj
C:\masm32\bin\link.exe
: 这是 MASM32 的链接器程序。
/SUBSYSTEM:WINDOWS
: **非常重要!**这个参数告诉链接器,要生成的是一个窗口程序,而不是一个控制台(黑框框)程序。
/LIBPATH:C:\masm32\lib
: 告诉链接器去哪里找库文件(比如kernel32.lib
)。
opencalc.obj
: 我们的输入目标文件。
执行后,如果没有报错,最终成品:opencalc.exe
悄咪咪记一下汇编应用小趣味
-
绕过系统调用限制
直接通过汇编调用 syscall,不过需要考虑不同Windows版本:; Linux下通过syscall启动计算器 mov rax, 59 ; sys_execve系统调用号 mov rdi, cmd ; 命令字符串 syscall
-
Shellcode动态查找函数地址
通过遍历PEB结构获取 kernel32.dll 基址:mov ebx, fs:[0x30] ; 获取PEB地址 mov ebx, [ebx+0x0C] ; PEB_LDR_DATA结构 mov ebx, [ebx+0x14] ; InMemoryOrderModuleList ; ... 遍历链表找到kernel32.dll基址
易语言
这是一道附加小吃。如果说 C 是操作系统的“母语”,Python 是“胶水语言”,那么易语言就像是一个专门为 Windows 系统打造的“图形化中文遥控器”。
它的核心哲学就是,让不懂英文、不理解复杂 API 的人,也能通过拖拽控件和使用中文命令来快速开发 Windows 程序。它与操作系统的交互方式,是把最常用的系统功能,封装成了一个个简单直白的中文命令。
下载参考https://blog.youkuaiyun.com/qq_42363090/article/details/136952430
有时候语言学多了,就会丧失人性… 搞错了,简单的反而一下子摸不到头脑
很抽象,下一步添加一个触发按钮
写(。・∀・)ノ゙嗨了,顺带编译一下
如果出现编译不成功,链接器路径没对