سجل التعديلات (النسخة الأولى، نُشرت في 2 Sep، 2026)
- النشر الأول
«نجحت في الاختبار، لكن الأداة الخارجية لا تبدأ على جهاز يحتوي مساره مسافة». «مرّرت C:\data\ فاندمجت مع الوسيط التالي في واحد». «مرّرت JSON وسيطاً فاختفت علامات الاقتباس وفشل الطرف الآخر في التحليل» ── أعطال تتكرّر في الشيفرة التي تشغّل عمليات ابنة. معظمها لا يأتي من المنطق بل من كتابة تفترض غير ما هو قائم: ليس في Windows آلية لتمرير «مصفوفة وسائط».
ما يتلقّاه CreateProcess، الدالّة التي تنشئ عملية على Windows، هو سلسلة واحدة تُسمَّى lpCommandLine. مهما أعدّ المستدعي المصفوفة بعناية، تُربَط دائماً في سلسلة واحدة عند عبور حدود نظام التشغيل، ويعيد الطرف المستقبل تقسيمها. يقرّر وقت تشغيل الطرف المستقبل قواعد التقسيم، ووقت تشغيل C وCommandLineToArgvW ووقت تشغيل .NET وcmd.exe شيفرات منفصلة. تمرير الوسائط يعني بناء سلسلة سيقسّمها محلّل الطرف الآخر إلى القطع الأصلية.
تتّخذ هذه المقالة موقف تشغيل عمليات ابنة من شيفرة Win32 و.NET، لا من سكربتات PowerShell، وترتب أين تُربَط السلسلة وأين تُقسَّم وأيّ القواعد تسري. جهة PowerShell (تغيير تمرير الوسائط في 7.3، و--%، و$PSNativeCommandArgumentPassing) مشمولة في «استدعاء exe خارجي صحيحاً من PowerShell»، لذا تحفر هذه المقالة الطبقة تحت ذلك.
flowchart TB
accTitle: الطبقة التي تغطّيها هذه المقالة
accDescr: تمرير وسائط PowerShell مشمول في مقالة أخرى؛ تغطّي هذه المقالة الطبقة تحت ذلك، من CreateProcess في Win32 وProcessStartInfo في .NET حتّى محلّل الـ exe المستهدف
ps["تمرير وسائط PowerShell (مقالة أخرى)"] --> net["ProcessStartInfo في .NET"]
net --> win["CreateProcessW في Win32"]
win --> str["سلسلة سطر أوامر واحدة"]
str --> parser["محلّل الـ exe المستهدف"]
net -.->|"نطاق هذه المقالة"| parser
الشكل 1: تحت PowerShell تقع طبقتا .NET وWin32، وأيّاً منهما شغّلت منه تكون النتيجة سلسلة واحدة. تغطّي هذه المقالة قواعد تلك الطبقة.
1. الخلاصة أوّلاً
- عملية Windows لا تتلقّى مصفوفة وسائط. تصل السلسلة الواحدة الممرَّرة إلى
CreateProcessإلى العملية الجديدة (قد يكمّل نظام التشغيل المسار الكامل لاسم الملفّ التنفيذي في المقدّمة فقط)، ويعيدهاGetCommandLineW.argvشيء يصنعه الطرف المستقبل بنفسه.1 2 - جوهر قواعد التقسيم ثلاثة بنود: التقسيم على المسافات وعلامات الجدولة، وعدم التقسيم داخل نطاق محاط بعلامتي اقتباس مزدوجتين، والـ backslash خاصّ فقط عندما تليه علامة اقتباس مزدوجة مباشرة (2n من الـ backslash تصير n مع فتح الإحاطة أو إغلاقها؛ و2n+1 تصير n مع علامة اقتباس حرفية).3 4
- يتبع الرمز في المقدّمة فقط (
argv[0]، اسم الملفّ التنفيذي) قاعدة مختلفة: يمكن إحاطته بعلامات اقتباس، لكن هروب الـ backslash لا يسري. إن كانlpApplicationNameهوNULLصار تفسير مسار يحتوي مسافات غامضاً وجُرِّبC:\Program.exeأوّلاً.1 4 - في جهة التجميع تكفي قاعدة واحدة: «إن احتوى الوسيط مسافة أو علامة اقتباس، أو كان فارغاً، فأحِطه بعلامات اقتباس، وضاعف الـ backslash الذي يسبق علامة اقتباس والـ backslash الختامي، واكتب علامات الاقتباس
\"». يفعلProcessStartInfo.ArgumentListفي .NET Core 2.1 فما بعده ذلك عنك.5 6 - لا تولّد الصيغة التي تضع علامتَي اقتباس متجاورتين داخل وسيط غير فارغ (مثل
"ab""c")، لأن المستقبلين يختلفون في تفسيرها. أمّا""التي تمثّل وسيطاً فارغاً فأمر آخر وهي صحيحة. يجلس cmd.exe والملفّات الدفعية خارج هذه القواعد، لذا لا تمرّر قيماً غير موثوقة عبرها.6 7 - الحدود 32,767 وحدة ترميز UTF-16 لـ
lpCommandLine(بما فيها الـ null الختامي؛ الأحرف التي هي أزواج بديلة مثل الرموز التعبيرية تُعدّ اثنتين) و8,191 حرفاً لـ cmd.exe. إن أوشكت على التجاوز فانتقل إلى ملفّ استجابة، لكن فقط عندما يستطيع الهدف قراءة واحد (أو يمكن إصلاحه ليقرأ واحداً) بنحو مثل@file.1 8
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 28، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. لا توجد مصفوفة وسائط ── CreateProcess والسلسلة الواحدة
المعامل الثاني لـ CreateProcessW، lpCommandLine، هو سلسلة واحدة منتهية بـ null تُصفّ فيها اسم الملفّ التنفيذي والوسائط مفصولة بمسافات. حدّ الطول 32,767 وحدة ترميز UTF-16 بما فيها الـ null الختامي (عدد عناصر wchar_t؛ حرف زوج بديل مثل رمز تعبيري يستهلك اثنتين لكلّ حرف، لذا لا تفحص مسبقاً بعدد الأحرف الظاهر)، ولأن الإصدار Unicode قد يعدّل هذه السلسلة فإن تمرير حرفي سلسلة أو مخزن const قد يسبّب انتهاك وصول.1
تُسلَّم هذه السلسلة إلى العملية الجديدة دون تغيير ضمن معاملات العملية، ويستخرجها الابن بـ GetCommandLineW. لأن نظام التشغيل قد يكمّل المسار الكامل لاسم الملفّ التنفيذي في المقدّمة، لا تطابق السلسلة التي يراها الابن بالضرورة السلسلة التي مرّرها الأب تماماً.2 أمّا lpCmdLine الممرَّر إلى WinMain لتطبيق واجهة فهو هذه السلسلة بعد حذف اسم البرنامج.9
flowchart TB
accTitle: المسار الذي تسلكه الوسائط حتّى تصل إلى العملية الابن
accDescr: تُربَط مصفوفة وسائط المستدعي في سلسلة واحدة في lpCommandLine لـ CreateProcess وتُمرَّر إلى العملية الجديدة، وتقسّم العملية الابن السلسلة التي تستخرجها بـ GetCommandLineW بمحلّلها الخاص لتنشئ argv
arr["مصفوفة وسائط المستدعي"] --> join["تُربَط في سلسلة واحدة (مسؤولية المستدعي)"]
join --> cp["lpCommandLine لـ CreateProcessW"]
cp --> peb["معاملات العملية الجديدة"]
peb --> gcl["السلسلة التي يعيدها GetCommandLineW"]
gcl --> parse["محلّل الطرف المستقبل يقسّمها"]
parse --> argv["مصفوفة argv / args"]
الشكل 2: المصفوفة لا تعبر الحدود. الربط مسؤولية المستدعي، والتقسيم مسؤولية المستقبل، ولا تُستعاد المصفوفة الأصلية إلّا حين تتّفق قواعد الطرفين.
النقطة التي تُمسَك هنا أن الربط والتقسيم يحدثان في عمليتين مختلفتين، في شيفرتين مختلفتين. لا يستطيع المستدعي الربط صحيحاً دون معرفة بما سيقسّم به الطرف الآخر، وليس للمستقبل سبيل لمعرفة كيف رُبطت السلسلة. على أنظمة شبيهة بـ Unix يقبل execve المصفوفة كما هي، لذا لا توجد هذه المشكلة. إنّها فرضية خاصّة بـ Windows، لكنّها تتبع كلّ تشغيل لعملية.
3. من يقسّم ── ثلاثة محلّلات
في الطرف المستقبل تأتي الشيفرة التي تقسّم السلسلة إلى argv أساساً في ثلاثة أنواع.
| الطرف المستقبل | الشيفرة التي تقسّم | متى تُستدعى |
|---|---|---|
main / wmain في C/C++ |
رمز بدء وقت تشغيل C في MSVC | ينشئ argc / argv تلقائياً عند بدء البرنامج4 |
| شيفرة تستخدم Win32 API مباشرة | CommandLineToArgvW |
تمرّر إليها قيمة عودة GetCommandLineW لتحويلها إلى صيغة argv3 |
Main(string[] args) / Environment.GetCommandLineArgs() في .NET (التكوين المعتاد الذي يُشغَّل عبر apphost أو dotnet.exe) |
رمز بدء وقت تشغيل C للمضيف (apphost / dotnet.exe) |
على Windows المضيف برنامج wmain؛ يأخذ argv الذي بناه وقت تشغيل C، ويزيل خياراته ومسار التطبيق، ويمرّر الباقي إلى وقت التشغيل مع مسار التطبيق. عند البدء يبني وقت التشغيل مصفوفة أوّل عناصرها اسم البرنامج (اسم التشغيل الممرَّر من المضيف، أو مسار التجميع إن لم يوجد) ويحتفظ بها لـ GetCommandLineArgs()، بينما يتلقّى Main في args الوسائط بعد حذف اسم البرنامج فقط10 11 12 |
| تكوين يحمّل وقت تشغيل .NET مكتبة مستضافة ولا يتلقّى وسائط بدء | شيفرة التقسيم الخاصّة بوقت تشغيل .NET (SegmentCommandLine) |
كاحتياط يقسّم GetCommandLineArgs() قيمة عودة GetCommandLineW بنفسه. منفَّذ ليطابق قواعد وقت تشغيل C ولا يستخدم CommandLineToArgvW، لأن ذلك «يسلك سلوكاً مختلفاً قليلاً»12 |
شيفرة التقسيم ثلاثة سلالات: رمز بدء وقت تشغيل C، وCommandLineToArgvW، وشيفرة التقسيم الخاصّة بوقت تشغيل .NET، وهي تنفّذ قواعد بالهيكل نفسه، لكنّها ليست الشيفرة نفسها. تطبيق .NET يُشغَّل عبر apphost أو dotnet.exe يُقسَّم في الواقع بقواعد السلالة الأولى (رمز بدء وقت تشغيل C)، لأن المضيف نفسه برنامج wmain مبني بوقت تشغيل C في MSVC. لا يزال مصدر وقت تشغيل .NET يحمل تعليقاً يقول إن CommandLineToArgvW لا يُستخدم لأن سلوكه يختلف قليلاً.12 تظهر الفروق عند الحواف، مثل معاملة "" المذكورة لاحقاً، ونادراً ما تصيبها الوسائط اليومية، لكن افتراض أن «القواعد واحدة فكلّ شيء يمرّ» هو ما ينكسر عند الحواف.
flowchart TB
accTitle: المحلّلات الثلاثة في الطرف المستقبل
accDescr: السلسلة الواحدة التي يعيدها GetCommandLineW يقسّمها رمز بدء وقت تشغيل C لـ C/C++، وCommandLineToArgvW للاستخدام المباشر لـ Win32، وشيفرة التقسيم الخاصّة بوقت التشغيل لـ .NET المحمَّل مكتبة مستضافة؛ يتبع كلّ منها قواعد بالهيكل نفسه لكنّه تنفيذ منفصل. يتلقّى تطبيق .NET معتاد يُشغَّل عبر apphost أو dotnet.exe المصفوفة التي قسّمها رمز بدء وقت تشغيل C للمضيف
s["سلسلة GetCommandLineW"] --> crt["رمز بدء وقت تشغيل C"]
s --> api["CommandLineToArgvW"]
s --> net["شيفرة التقسيم الخاصّة بـ .NET (عند التحميل بمضيف)"]
crt --> app[".NET عبر apphost / dotnet.exe هو نفسه"]
crt --> same["هيكل القواعد نفسه، تنفيذات منفصلة"]
api --> same
net --> same
الشكل 3: لشيفرة التقسيم ثلاث سلالات. يتلقّى تطبيق .NET يُشغَّل عبر apphost أو dotnet.exe المصفوفة التي قسّمها رمز بدء وقت تشغيل C للمضيف، وشيفرة التقسيم الخاصّة بوقت التشغيل احتياط لتكوين المكتبة المستضافة. لأنك لا تستطيع أن تعرف من الخارج أيّها يشغّل الـ exe المستهدف، الجواب العملي بناء سلسلة تعطي النتيجة نفسها عليها كلّها.
لاحظ أن args في Main(string[] args) في .NET لا يتضمّن اسم البرنامج، بينما يتضمّن العنصر الأوّل لـ Environment.GetCommandLineArgs() اسم البرنامج. يحتلّ الأخير الموقع نفسه لـ argv[0] في C/C++.13 في تشغيل معتاد مثل dotnet app.dll x يزيل المضيف خيارات المضيف ومسار التطبيق (dotnet.exe وapp.dll)، ولا يصل إلى args في Main سوى x.14 أمّا GetCommandLineArgs() فيعيد المصفوفة التي قدّم وقت التشغيل إليها اسم البرنامج عند البدء (مسار app.dll يليه x).11 لا تقسّم شيفرة التقسيم الخاصّة بوقت التشغيل GetCommandLineW إلّا في تكوين المكتبة المستضافة الذي لا يتلقّى وسائط بدء؛ في تكوين يمرّر فيه مضيف أصلي argc/argv الخاصّ به ويستدعي Main، يكون args في Main ما مرّره المضيف.
4. قواعد التقسيم ── المسافات وعلامات الاقتباس والـ backslash
إليك القواعد التي يشترك فيها المحلّلات الثلاثة، لـ argv[1] فما بعده.3 4
- تُفصَل الوسائط بـ مسافات أو علامات جدولة.
- يصير النطاق المحاط بـ علامتي اقتباس مزدوجتين وسيطاً واحداً حتّى إن احتوى مسافات. علامات الاقتباس نفسها ليست جزءاً من الوسيط. قد تبدأ علامة الاقتباس في وسط وسيط، وإن انتهت السلسلة بلا علامة إغلاق صار كلّ ما إلى النهاية الوسيط الأخير.
- يُعامَل الـ backslash حرفاً عادياً. فقط عندما تليه علامة اقتباس مزدوجة مباشرة تسري القواعد التالية.
- إن سبق علامة اقتباس مزدوجة 2n من الـ backslash، خرج n من الـ backslash، وعملت علامة الاقتباس «بداية الإحاطة أو نهايتها».
- إن سبق علامة اقتباس مزدوجة 2n+1 من الـ backslash، خرج n من الـ backslash وعلامة اقتباس حرفية، ولم تتغيّر حالة الإحاطة.
- علامة الإقحام (
^) ليست حرف هروب (تلك قاعدة cmd.exe، لا قاعدة المحلّل).
يحفظ المحلّل بتّة حالة واحدة، «هل أنا داخل علامات اقتباس»، ويقلبها عند كلّ علامة اقتباس، ويقرأ السلسلة من اليسار إلى اليمين. ما إذا كانت المسافة تفصل الوسائط يُقرَّر بهذه الحالة.
flowchart TB
accTitle: تدفّق التقسيم الذي يتبدّل بين داخل علامات الاقتباس وخارجها
accDescr: خارج علامات الاقتباس يفصل المحلّل الوسائط على المسافات؛ وعندما يلقى علامة اقتباس يدخل ويُعامل المسافات جزءاً من الوسيط؛ وعندما يلقى علامة أخرى يعود إلى الخارج. يُعامَل الـ backslash معاملة خاصّة فقط عندما تليه علامة اقتباس مباشرة
out["خارج علامات الاقتباس: التقسيم على المسافات"] -->|"تُلقى علامة اقتباس"| inq["داخل علامات الاقتباس: المسافات جزء من الوسيط"]
inq -->|"تُلقى علامة اقتباس"| out
out -->|"يلي الـ backslash علامة اقتباس مباشرة"| bs["تطبيق قاعدة الـ backslash"]
inq -->|"يلي الـ backslash علامة اقتباس مباشرة"| bs
bs -->|"2n: أخرج n وافتح/أغلق"| toggle["اقلب حالة الإحاطة"]
bs -->|"2n+1: أخرج n وعلامة اقتباس حرفية"| lit["أبقِ حالة الإحاطة"]
الشكل 4: جوهر التقسيم يُقرَّر ببتّة واحدة فقط، «داخل علامات الاقتباس أم خارجها»، وبعدد الـ backslash مباشرة قبل علامة الاقتباس.
بدلاً من حفظ القواعد نثراً، أضمن أن تنظر إلى التقابل بين الدخل والخرج.
| جزء من سطر الأوامر (دخل) | الوسائط الناتجة | القاعدة العاملة |
|---|---|---|
a b c |
a، b، c |
التقسيم على المسافات |
"a b" c |
a b، c |
نطاق محاط لا يُقسَّم |
C:\data\ next |
C:\data\، next |
لا تلي الـ backslash علامة اقتباس، فهو حرف عادي |
"C:\data\\" next |
C:\data\، next |
الاثنان قبل علامة الاقتباس يصيران واحداً، وتُغلق علامة الاقتباس |
"C:\data\" next |
C:\data" next |
backslash واحد، فتصير علامة الاقتباس حرفية ولا تُغلق الإحاطة أبداً فتبتلع الوسيط التالي |
"say \"hi\"" |
say "hi" |
عدد فردي، فعلامات اقتباس حرفية |
"" |
سلسلة فارغة | الطريقة الوحيدة لتمرير وسيط فارغ |
'a b' |
'a، b' |
لعلامات الاقتباس المفردة لا معنى خاصّ15 |
الصفّ الخامس هو حقيقة «مرّرت C:\data\ فاندمجت مع الوسيط التالي في واحد» من المقدّمة. في اللحظة التي تحيط فيها مساراً ينتهي بـ backslash بعلامات اقتباس، تتحوّل علامة الإغلاق إلى حرف ولا تُغلق الإحاطة أبداً.
flowchart TB
accTitle: كيف يبتلع الـ backslash الختامي الوسيط التالي
accDescr: عندما يُحاط مسار ينتهي بـ backslash بعلامات اقتباس، تجلس علامة الاقتباس التي كان ينبغي أن تغلق مباشرة بعد backslash واحد فتُفسَّر علامة اقتباس حرفية، فلا تُغلق الإحاطة ويُقرأ كلّ شيء حتّى الوسيط التالي وسيطاً واحداً
a["مسار محاط ينتهي بـ backslash واحد"] --> b["عدد فردي من الـ backslash يسبق علامة الإغلاق"]
b --> c["تُخرَج علامة الاقتباس حرفاً ولا تُغلق الإحاطة"]
c --> d["المسافات اللاحقة لم تعد تفصل"]
d --> e["يصل كلّ شيء حتّى الوسيط التالي وسيطاً واحداً"]
a -.->|"ضاعف الـ backslash"| ok["تُغلق الإحاطة وتنفصل الوسائط"]
الشكل 5: لماذا يلزم «مضاعفة الـ backslash الختامي». الإحاطة المكتوبة دون معرفة القواعد تنكسر عند نهاية المسار.
«علامتا اقتباس متتاليتان داخل الإحاطة»، حيث تختلف التنفيذات
لقواعد وقت تشغيل C في MSVC بند آخر: «تُعامَل علامتَا اقتباس متتاليتان داخل سلسلة محاطة كعلامة واحدة» (صيغة مثل "ab""c"، وهي أمر منفصل عن "" التي تمثّل وسيطاً فارغاً).4 لكن القواعد الرسمية لـ CommandLineToArgvW لا تحتوي هذا البند، وشيفرة التجميع في وقت تشغيل .NET تتجنّب صراحة توليد هذه الصيغة لأن «علامة اقتباس تلي علامة إغلاق يفسّرها VC قبل 2008 وبعده على نحو مختلف».6
كمستقبل يكفي أن تعرف أن مثل هذا الدخل قد يصل. كمجمّع، عندما تريد تمرير علامة اقتباس حرفاً، استخدم صيغة \" فقط. تعطي النتيجة نفسها على كلّ محلّل.
5. argv[0] يتبع قاعدة مختلفة ── lpApplicationName ومشكلة Program.exe
الرمز في المقدّمة، أي اسم الملفّ التنفيذي، خارج القواعد حتّى الآن. يُفترض أنّه سلسلة صالحة كمسار نظام ملفّات، لذا يمكن إحاطته بعلامات اقتباس لتضمين مسافات، لكن قواعد هروب الـ backslash لا تسري. ولا سبيل لتضمين علامة اقتباس نفسها في argv[0].4 3 تعامل شيفرة التجميع في .NET العنصر الأوّل أيضاً على حدة: «أحِطه بعلامات اقتباس إن وُجدت مسافات، وألقِ استثناء إن احتوى علامة اقتباس».6
ما يصير مشكلة في جهة الاستدعاء هو السلوك عندما يكون lpApplicationName لـ CreateProcess هو NULL. في تلك الحالة يُستنتَج الوحدة المراد تنفيذها من الرمز المفصول بمسافات في المقدّمة لـ lpCommandLine. عندما يحتوي المسار مسافات تنشأ مرشّحات عدّة، ويجرّبها نظام التشغيل بدءاً من الأقصر.1
flowchart TB
accTitle: ترتيب تخمين الملفّ التنفيذي عندما يكون lpApplicationName هو NULL
accDescr: إن مُرِّر C:\Program Files\MyApp -L -S بلا علامات اقتباس، يختبر CreateProcess وجود C:\Program.exe ثم C:\Program Files\MyApp.exe بذلك الترتيب، فإن وُضع C:\Program.exe هناك جرى ذلك
in["تمرير مسار بلا علامات اقتباس (يحتوي مسافات) في lpCommandLine"] --> t1["المرشّح 1: جرّب C:\Program.exe"]
t1 -->|"موجود"| bad["يبدأ ملفّ تنفيذي غير مقصود"]
t1 -->|"غير موجود"| t2["المرشّح 2: جرّب C:\Program Files\MyApp.exe"]
t2 --> ok["يبدأ الملفّ التنفيذي المقصود"]
in -.->|"مرّر lpApplicationName، أو أحِط الرمز في المقدّمة"| ok
الشكل 6: وضع مسار يحتوي مسافات في البداية بلا علامات اقتباس يجعل نظام التشغيل يجرّب المرشّحات من الأقصر. تسمّي الوثائق الرسمية هذا «خطراً» بعبارات صريحة.
تنصّ الوثائق الرسمية على أنّه إن وُضع C:\Program.exe هناك جرى بدل التطبيق المقصود، وتطلب ألّا تمرّر NULL لـ lpApplicationName، وإن فعلت فأحِط المسار في المقدّمة بعلامات اقتباس.1 عملياً افعل الاثنين. مرّر المسار الكامل للملفّ التنفيذي في lpApplicationName، وضع أيضاً المسار نفسه، محاطاً بعلامات اقتباس، في بداية lpCommandLine. عندما يُمرَّر الاثنان تُقرَّر الوحدة التي تعمل بـ lpApplicationName، ويصير argv[0] للعملية الابن الرمز في مقدّمة lpCommandLine. ما لم تُبقِ الاثنين متّسقين بالعرف، تنكسر الشيفرة التي تستخرج مسارها من argv[0]. الطريقة الموثوقة للحصول على مسارك هي GetModuleFileNameW.4
flowchart TB
accTitle: كيف تُقرَّر الوحدة المنفَّذة وargv[0]
accDescr: عندما يُمرَّر lpApplicationName وlpCommandLine معاً تُقرَّر الوحدة التي تعمل بـ lpApplicationName ويكون argv[0] للابن الرمز في مقدّمة lpCommandLine. تنكسر الشيفرة التي تستخرج مسارها من argv[0] عندما يتباعد الاثنان، لذا خذ مسارك بـ GetModuleFileNameW
app["lpApplicationName"] --> run["الوحدة التي تعمل"]
cl["الرمز في مقدّمة lpCommandLine"] --> a0["argv[0] للابن"]
a0 -.->|"تنكسر عندما يتباعدان"| self["شيفرة تستخرج مسارها من argv[0]"]
self -.->|"استخدم بدلاً منه"| gmf["GetModuleFileNameW"]
الشكل 7: «ما يعمل» و«ما يدخل argv[0]» يُقرَّران على حدة. تصميم يستخرج مساره من argv[0] لا يقوم فوق هذا الفصل.
نقطة أخرى: عندما يكون lpApplicationName هو NULL يُقيَّد جزء اسم الملفّ التنفيذي من lpCommandLine بـ MAX_PATH.1 لمعاملة المسارات الطويلة انظر «MAX_PATH ومطبّات المسارات وأسماء الملفّات في Windows».
6. قواعد جهة التجميع ── تكفي دالّة واحدة
ما إن تعرف قواعد التقسيم حتّى تستطيع بناء «سلسلة سيقسّمها الطرف الآخر إلى الأصل» بتشغيلها عكسياً فحسب. لكلّ وسيط من argv[1] فما بعده افعل ما يلي.6
- إن لم يكن فارغاً ولم يحتوِ مسافات ولا علامات اقتباس، ضعه كما هو.
- وإلّا فأحِط الكلّ بعلامات اقتباس. داخل الإحاطة،
- حوّل تتالياً من k من الـ backslash مباشرة قبل علامة اقتباس إلى 2k+1 ثم ضع علامة الاقتباس (جعل العدد فردياً يحوّله إلى «علامة اقتباس حرفية»)؛
- حوّل تتالياً من k من الـ backslash الختامي إلى 2k (تسبق علامة الإغلاق، فعدد زوجي يحوّله إلى «نهاية الإحاطة»)؛
- اترك كلّ backslash آخر كما هو.
- ضع سلسلة فارغة كـ
"".
flowchart TB
accTitle: تدفّق القرار لتجميع وسيط واحد
accDescr: إن كان الوسيط غير فارغ ولا يحتوي مسافات ولا علامات اقتباس فضعه كما هو؛ وإلّا فأحِطه بعلامات اقتباس، واجعل الـ backslash قبل علامة اقتباس 2k+1 والـ backslash الختامي 2k، وقدّم علامات الاقتباس بـ backslash، ثم أغلق
s["تلقَّ وسيطاً واحداً"] --> q{"فارغ، أو يحتوي مسافة أو علامة اقتباس؟"}
q -->|"لا"| raw["ضعه كما هو"]
q -->|"نعم"| open["علامة اقتباس افتتاح"]
open --> scan["امسح من اليسار"]
scan --> bq["k من الـ backslash قبل علامة اقتباس ← 2k+1"]
scan --> be["k من الـ backslash الختامي ← 2k"]
scan --> other["كلّ شيء آخر كما هو"]
bq --> close["علامة اقتباس إغلاق"]
be --> close
other --> close
الشكل 8: التجميع عكس قواعد التقسيم. لا توجد سوى ثلاثة فروع، وتضبط عدد الـ backslash فقط عند النهاية ومباشرة قبل علامة اقتباس؛ بذلك تدور أيّ سلسلة ذهاباً وإياباً، بشرط أن يقسّم الطرف المستقبل الأحرف العريضة بقواعد التقسيم نفسها لـ CommandLineToArgvW ووقت تشغيل C و.NET (الفصل 4) (هدف يفسّر سطر الأوامر الخام بنحو خاص، أو محلّل صدفة بينهما، خارج النطاق)، ولم يفعّل توسيع أحرف البدل مثل wsetargv.obj، ولا تحتوي السلسلة أحرف NUL، وتستوعب السلسلة المجمَّعة كاملة حدّ lpCommandLine (32,767 وحدة ترميز UTF-16 بما فيها الـ null الختامي). (سطر الأوامر سلسلة منتهية بـ null، لذا حرف NUL هو الشيء الوحيد الذي لا يمكن تمريره من حيث المبدأ. على هدف فُعِّل فيه توسيع أحرف البدل يُستبدل وسيط يحتوي * أو ? بأسماء ملفّات؛ انظر الفصل 8. سلسلة فوق الحدّ يرفضها CreateProcessW؛ انظر الفصل 10.)
تعكس هذه القاعدة اللاتناظر «الـ backslash خاصّ فقط مباشرة قبل علامة اقتباس» كما هو. لا حاجة لمضاعفة الـ backslash الذي يفصل مكوّنات المسار آلياً؛ النقطة أنك تلمس فقط تلك مباشرة قبل علامة اقتباس وعند النهاية.
7. التنفيذ في .NET ── ArgumentList وArguments
في ProcessStartInfo من .NET Core 2.1 فما بعده يوجد ArgumentList الذي يتولّى هذا التجميع. العنصر الواحد وسيط واحد، والسلاسل المضافة لا تحتاج هروباً مسبقاً، وعند Process.Start يجمّع .NET داخلياً سلسلة واحدة ويسلّمها إلى نظام التشغيل.5
var psi = new ProcessStartInfo
{
FileName = @"C:\Program Files\MyTool\convert.exe",
UseShellExecute = false,
};
psi.ArgumentList.Add("--input");
psi.ArgumentList.Add(inputPath); // يجوز أن يحتوي مسافات وbackslash ختامياً وعلامات اقتباس
psi.ArgumentList.Add("--output");
psi.ArgumentList.Add(outputPath);
psi.ArgumentList.Add("--label");
psi.ArgumentList.Add(""); // الوسيط الفارغ أيضاً يُمرَّر صحيحاً كـ ""
using var proc = Process.Start(psi)
?? throw new InvalidOperationException("أعاد Process.Start القيمة null");
proc.WaitForExit();
if (proc.ExitCode != 0)
throw new InvalidOperationException($"فشل convert.exe (ExitCode={proc.ExitCode})");
Arguments خاصّية تمرّر سلسلة واحدة بنيتها بنفسك كما هي. الاثنتان مستقلّتان، وعندما تستخدم إحداهما يجب أن تكون الأخرى فارغة.16 وترشد الوثائق الرسمية أيضاً إلى اختيار ArgumentList إن لم تكن واثقاً من الإحاطة.5
flowchart TB
accTitle: أين يصير ArgumentList وArguments سلسلة
accDescr: يهرب .NET عناصر ArgumentList عنصراً عنصراً ويجمّعها سلسلة واحدة ثم يمرّرها إلى CreateProcess، بينما تمرّر Arguments السلسلة التي جمّعها المستدعي كما هي. كلاهما سلسلة واحدة عند وصوله إلى نظام التشغيل
al["ArgumentList (عنصر واحد = وسيط واحد)"] --> esc["يهرب .NET كلّ عنصر ويربط"]
ar["Arguments (سلسلة واحدة جمّعتها بنفسك)"] --> pass["كما هي"]
esc --> cmd["سلسلة سطر أوامر واحدة"]
pass --> cmd
cmd --> cp["CreateProcess"]
الشكل 9: أيّاً استخدمت فما يصل إلى نظام التشغيل سلسلة واحدة. الفرق «من يجمّع» فقط، وإيكال ذلك إلى من يعرف القواعد هو ArgumentList.
شيفرة تجميع ArgumentList هي قواعد الفصل 6 نفسها. إن لم يكن فارغاً ولم يحتوِ مسافات ولا علامات اقتباس وُضع كما هو، وإلّا أُحيط بعلامات اقتباس، وصار الـ backslash قبل علامة اقتباس مضاعفاً زائداً واحداً، والـ backslash الختامي مضاعفاً، وقُدِّمت علامات الاقتباس دائماً بـ backslash. لا تولّد صيغة علامات اقتباس متجاورة داخل وسيط غير فارغ. الوسيط الفارغ وحده يُصفّ كـ ""، وهذه الكتابة الصحيحة.6
على .NET Framework تجمّع بنفسك
ArgumentList واجهة من .NET Core 2.1 فما بعده، ولا توجد في ProcessStartInfo لـ .NET Framework.5 في تطبيقات .NET Framework 4.8، والأدوات الداخلية المبنية عليها، اكتب قواعد الفصل 6 بنفسك ومرّرها إلى Arguments.
// لـ .NET Framework. تجميع السلسلة الواحدة الممرَّرة إلى ProcessStartInfo.Arguments.
// القواعد نفسها التي يستخدمها ProcessStartInfo.ArgumentList داخلياً.
static string BuildArguments(IEnumerable<string> args)
{
var sb = new StringBuilder();
foreach (var arg in args)
{
if (sb.Length > 0) sb.Append(' ');
AppendArgument(sb, arg);
}
return sb.ToString();
}
static void AppendArgument(StringBuilder sb, string arg)
{
if (arg.IndexOf('\0') >= 0)
throw new ArgumentException("لا يمكن أن يحتوي الوسيط حرف NUL (سطر الأوامر سلسلة منتهية بـ null، فينقطع هناك)");
bool needsQuote = arg.Length == 0 || arg.Any(c => char.IsWhiteSpace(c) || c == '"');
if (!needsQuote)
{
sb.Append(arg); // كما هو
return;
}
sb.Append('"');
int i = 0;
while (i < arg.Length)
{
int backslashes = 0;
while (i < arg.Length && arg[i] == '\\') { i++; backslashes++; }
if (i == arg.Length)
{
sb.Append('\\', backslashes * 2); // الختام: مباشرة قبل علامة الإغلاق لذا مضاعف
}
else if (arg[i] == '"')
{
sb.Append('\\', backslashes * 2 + 1).Append('"'); // قبل علامة اقتباس: مضاعف + 1
i++;
}
else
{
sb.Append('\\', backslashes).Append(arg[i]); // غير ذلك: كما هو
i++;
}
}
sb.Append('"');
}
نضع الدخل والخرج جنباً إلى جنب.
| القيمة المراد تمريرها | السلسلة التي يخرجها AppendArgument |
|---|---|
strict |
strict |
| سلسلة فارغة | "" |
C:\Program Files\input |
"C:\Program Files\input" |
C:\Program Files\input\ |
"C:\Program Files\input\\" |
say "hi" |
"say \"hi\"" |
a\"b |
"a\\\"b" |
C:\data\ (بلا مسافات) |
C:\data\ |
انتبه إلى الصفّ الأخير. القيمة التي لا تحتوي مسافات ولا علامات اقتباس لا تُحاط، لذا يخرج الـ backslash الختامي كما هو. إن لم تُحِط لا تُفعَّل القاعدتان 4 و5، وبذلك يصل C:\data\ صحيحاً.
flowchart TB
accTitle: اختيار وسيلة التجميع حسب إصدار .NET
accDescr: على .NET Core 2.1 فما بعده أَوكِل الأمر إلى ProcessStartInfo.ArgumentList؛ وعلى .NET Framework جمّع سلسلة Arguments بدالّتك وفق القواعد نفسها. في الحالين لا تكتب علامات اقتباس يدوياً بربط سلاسل
v{"ما إصدار .NET؟"}
v -->|"Core 2.1 فما بعده"| al["أضف إلى ArgumentList عنصراً عنصراً"]
v -->|"Framework"| own["جمّع Arguments بدالّتك"]
al --> no["لا تكتب علامات اقتباس يدوياً"]
own --> no
الشكل 10: وسيلتان، مبدأ واحد. التزم «لا تكتب علامات اقتباس يدوياً» فلا يحدث الانكسار عند نهاية المسار.
لاحظ أنّه مع UseShellExecute = true يمرّ التشغيل عبر ShellExecuteEx لا CreateProcess، وتصير محتويات ArgumentList المعاملات الممرَّرة إلى الصدفة. عند فتح مستند أو عنوان URL يبني ارتباط الملفّ سطر أوامر المعالج الفعلي، لذا لا تصل السلسلة التي بنيتها هنا إلى الهدف كما هي بالضرورة. للاستخدامات التي تعيد توجيه الخرج أو تحتاج رمز الخروج بموثوقية اضبط UseShellExecute = false وصمّم الشيفرة لتقرأ الخرج القياسي والخطأ القياسي في الوقت نفسه. ذلك الجزء مشمول في «قائمة تحقّق للتعامل الآمن مع العمليات الابنة في تطبيقات Windows».
8. التنفيذ في C++ / Win32
في C++ تكتب الجهتين بنفسك، التجميع والتقسيم. للتجميع حوّل قواعد الفصل 6 مباشرة إلى دالّة.
#include <windows.h>
#include <string>
#include <stdexcept>
#include <string_view>
#include <vector>
// يلحق وسيطاً واحداً لـ argv[1] فما بعده. القواعد عكس قواعد تقسيم CommandLineToArgvW / CRT.
void AppendArgument(std::wstring& cmd, std::wstring_view arg)
{
if (!cmd.empty()) cmd += L' ';
if (arg.find(L'\0') != std::wstring_view::npos)
throw std::invalid_argument("لا يمكن أن يحتوي الوسيط حرف NUL (سطر الأوامر سلسلة منتهية بـ null فينقطع هناك)");
const bool needsQuote =
arg.empty() || arg.find_first_of(L" \t\"") != std::wstring_view::npos;
if (!needsQuote) { cmd += arg; return; }
cmd += L'"';
for (size_t i = 0; ; ) {
size_t backslashes = 0;
while (i < arg.size() && arg[i] == L'\\') { ++i; ++backslashes; }
if (i == arg.size()) {
cmd.append(backslashes * 2, L'\\'); // الختام: مضاعف
break;
}
if (arg[i] == L'"') {
cmd.append(backslashes * 2 + 1, L'\\'); // قبل علامة اقتباس: مضاعف + 1
cmd += L'"';
} else {
cmd.append(backslashes, L'\\'); // غير ذلك: كما هو
cmd += arg[i];
}
++i;
}
cmd += L'"';
}
// argv[0] (الملفّ التنفيذي) يتبع قاعدة مختلفة: أحِطه بعلامات اقتباس فقط إن وُجدت مسافات. لا يمكن أن يحتوي علامة اقتباس.
std::wstring QuoteArgv0(std::wstring_view exe)
{
if (exe.find(L'\0') != std::wstring_view::npos)
throw std::invalid_argument("لا يمكن أن يحتوي مسار الملفّ التنفيذي حرف NUL (سينقطع lpApplicationName وسطر الأوامر هناك، وقد يُشغَّل المسار حتّى تلك النقطة)");
if (exe.find(L'"') != std::wstring_view::npos)
throw std::invalid_argument("لا يمكن أن يحتوي مسار الملفّ التنفيذي علامة اقتباس");
if (exe.empty() || exe.find_first_of(L" \t") != std::wstring_view::npos)
return L'"' + std::wstring(exe) + L'"';
return std::wstring(exe);
}
في الاستدعاء مرّر المسار الكامل للملفّ التنفيذي في lpApplicationName ومخزناً قابلاً للكتابة في lpCommandLine.
const std::wstring exe = LR"(C:\Program Files\MyTool\convert.exe)";
std::wstring cmd = QuoteArgv0(exe); // أبقِ argv[0] متّسقاً مع الملفّ التنفيذي
AppendArgument(cmd, L"--input");
AppendArgument(cmd, inputPath);
AppendArgument(cmd, L"--output");
AppendArgument(cmd, outputPath);
std::vector<wchar_t> buffer(cmd.begin(), cmd.end());
buffer.push_back(L'\0'); // قد يعدّل CreateProcessW السلسلة
STARTUPINFOW si{}; si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
if (!CreateProcessW(exe.c_str(), // lpApplicationName: لا تجعله NULL أبداً
buffer.data(), // lpCommandLine: يبدأ بالمسار نفسه محاطاً
nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT,
nullptr, nullptr, &si, &pi)) {
const DWORD err = GetLastError();
// سجّل err هنا وأعده إلى المستدعي. لا تبتلعه
return;
}
CloseHandle(pi.hThread); // مقبض الخيط الرئيسي غير مطلوب، لذا أغلقه أوّلاً
switch (WaitForSingleObject(pi.hProcess, INFINITE)) { // أضف مهلة إن لزم
case WAIT_OBJECT_0: { // خرج. اقرأ رمز الخروج في هذا الفرع فقط
DWORD exitCode = 0;
if (!GetExitCodeProcess(pi.hProcess, &exitCode)) {
const DWORD err = GetLastError();
// سجّل فشل الاستخراج أيضاً، وأعده إلى المستدعي كفشل
} else if (exitCode != 0) {
// بدأ الهدف لكن معالجته فشلت. لا تعامله كـ 0؛
// سجّل رمز الخروج وأعده إلى المستدعي (مثل فحص ExitCode في مثال C#)
}
break;
}
case WAIT_TIMEOUT:
// لا يزال يعمل. استدعاء GetExitCodeProcess هنا يعيد STILL_ACTIVE (259) فقط،
// وليس رمز خروج. يأخذ هذا المثال سياسة «اطوِ المهلة في فشل»:
// فقط عندما يمرّ طلب الإنهاء نرى خروجه، ثم نمضي إلى CloseHandle أدناه.
// إن كانت السياسة مواصلة الانتظار فلا تكسر هنا وتغلق المقابض (ذلك
// يُفلت الابن وهو لا يزال يعمل). عُد إلى الانتظار
if (!TerminateProcess(pi.hProcess, 1)) {
const DWORD err = GetLastError();
// تعذّر إنهاؤه (حقوق غير كافية إلخ). الانتظار بـ INFINITE هنا يجعل
// المهلة المضافة لمنع التجاوز بلا معنى. سجّل err وأعد فشلاً إلى
// المستدعي دون انتظار (يُفلت الابن وهو لا يزال يعمل، لذا سجّل ذلك أيضاً)
break;
}
WaitForSingleObject(pi.hProcess, INFINITE); // مرّ طلب الإنهاء، فاشهد خروجه قبل الإغلاق
// أعد المهلة إلى المستدعي كفشل
break;
default: { // WAIT_FAILED
const DWORD err = GetLastError();
// سجّل فشل الانتظار نفسه أيضاً
break;
}
}
CloseHandle(pi.hProcess); // نسيان هذا يسرّب مقبضاً واحداً لكلّ تشغيل
flowchart TB
accTitle: تقسيم الأدوار بين المعاملين الممرَّرين إلى CreateProcessW
accDescr: يثبّت lpApplicationName الوحدة المراد تنفيذها، ويقرّر lpCommandLine السلسلة التي تتلقّاها العملية الابن عبر GetCommandLineW. مرّر lpCommandLine مخزناً قابلاً للكتابة وأبقِ argv[0] في المقدّمة متّسقاً مع lpApplicationName
app["lpApplicationName: المسار الكامل للملفّ التنفيذي"] --> mod["تُثبَّت الوحدة المراد تنفيذها"]
cl["lpCommandLine: مخزن قابل للكتابة"] --> child["السلسلة التي يتلقّاها الابن عبر GetCommandLineW"]
child --> a0["الرمز في المقدّمة = argv[0]"]
a0 -.->|"أبقِ متّسقاً"| app
child --> rest["الباقي = وسائط مجمَّعة بقواعد الفصل 6"]
الشكل 11: «ماذا يُنفَّذ» و«ماذا يُمرَّر» يُقرَّران بمعاملين مختلفين. اجعل الاثنين صريحين فلا تحدث مشكلة Program.exe ولا انتهاك الوصول من مخزن غير قابل للكتابة.
في الطرف المستقبل مرّر قيمة عودة GetCommandLineW إلى CommandLineToArgvW لتحصل عليها بصيغة argv. حرّر قيمة العودة بـ LocalFree واحد. هناك سلوكيات حافّة: إن كان lpCmdLine سلسلة فارغة أُعيد مسار الملفّ التنفيذي الحالي، وإن بدأ بمسافة صار الوسيط الأوّل سلسلة فارغة.3
int argc = 0;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (argv == nullptr) {
const DWORD err = GetLastError();
// سجّل فشل التحليل أيضاً
return 1;
}
for (int i = 0; i < argc; ++i) {
// argv[0] اسم الملفّ التنفيذي. قد يكون نظام التشغيل قد كمّل المسار الكامل
}
LocalFree(argv);
إن استخدمت main / wmain فعل وقت تشغيل C الشيء نفسه عنك عند البدء. لكن لاحظ أن argv في main سلسلة ضيّقة محوَّلة إلى صفحة الرموز الحالية، لذا تُفقَد هنا الأحرف التي لا تستطيع صفحة الرموز تمثيلها (مثلاً مسار ياباني على جهاز خارج بيئة يابانية). «تدور ذهاباً وإياباً» دالّة التجميع في الفصل 6 مقابل مستقبلين يقسّمون الأحرف العريضة كما هي، مثل wmain وCommandLineToArgvW و.NET. افتراضياً لا تُوسَّع أحرف البدل، لكن ربط setargv.obj (wsetargv.obj لـ wmain) يجعله يوسّع * و?.4 إن مرّرت وسيطاً يحتوي * في اسم ملفّ إلى هدف بذلك الإعداد، اختلفت الوسائط التي تصل عمّا قصدت.
9. عندما يتوسّط cmd.exe والملفّات الدفعية
القواعد حتّى الآن تسري عندما تذهب السلسلة مباشرة من CreateProcess إلى الـ exe المستهدف. عندما يجلس cmd.exe بينهما تُضاف مرحلة تفسير أخرى.
يعامل cmd.exe & و| و( و) نحواً، ولتمريرها وسائط يجب الهروب بـ ^ أو الإحاطة بعلامات اقتباس. لمعاملة علامات الاقتباس في السلسلة التالية لـ /c أو /k قواعد خاصّة، ويتغيّر «ما إذا نُزعت علامات الاقتباس الخارجية» بوجود /s وعدد علامات الاقتباس ووجود أحرف خاصّة.17 علاوة على ذلك يتلقّى الملفّ الدفعي الوسائط غير مقسَّمة بل سلسلة سطر أوامر خام. تحذّر وثائق PowerShell الرسمية بوضوح من تمرير دخل غير موثوق إلى ملفّات دفعية.7 تقول وثائق CreateProcess إن تشغيل ملفّ دفعي يتمّ بتحديد cmd.exe في lpApplicationName وتمرير /c مع اسم الدفعة، ثم تلاحظ أن فريق هندسة MSRC لا يوصي بهذا، مع رابط إلى شرح MS14-019.1 ما أصلحه MS14-019 هو المشكلة أنّه عندما مُرِّر ملفّ دفعي مباشرة إلى CreateProcess بُحث عن cmd.exe في الدليل الحالي أوّلاً وأمكن اختطافه، وتوصية MSRC «تمرير المسار المؤهَّل بالكامل لـ cmd.exe وجعل الملفّ الدفعي وسيطه».18 أي أن المشكلة تشغيل ملفّ دفعي دون تسمية cmd.exe بمساره الكامل (ضبط lpApplicationName على NULL وترك اسم الدفعة يبدأه)، لا تشغيل /c نفسه مع المسار الكامل لـ cmd.exe في lpApplicationName.
flowchart TB
accTitle: توسّط cmd.exe يزيد مراحل التفسير
accDescr: تشغيل الـ exe المستهدف مباشرة يعني أن التقسيم يحدث مرّة واحدة في محلّل الهدف، لكن المرور عبر cmd.exe /c يضيف تفسير نحو cmd.exe، والملفّ الدفعي فوق ذلك يتلقّى السلسلة الخام، فتتغيّر قواعد الإحاطة في كلّ مرحلة
direct["عمليتك ← الـ exe المستهدف"] --> p1["تقسيم مرّة واحدة، بمحلّل الهدف فقط"]
p1 ~~~ via
via["عمليتك ← cmd.exe /c ← الـ exe المستهدف"] --> p2["يُضاف تفسير نحو cmd.exe (ampersand والأنبوب والأقواس وعلامة الإقحام)"]
p2 --> p3["التقسيم بمحلّل الهدف"]
p3 ~~~ bat
bat["عمليتك ← cmd.exe /c ← ملفّ دفعي"] --> p4["يتلقّى الملفّ الدفعي السلسلة الخام"]
p4 --> danger["تمرير قيم غير موثوقة عبره يصير حقن أوامر"]
الشكل 12: كلّما زادت المراحل اختلطت القواعد. شغّل مباشرة ما يمكن تشغيله مباشرة، ولا تمرّر قيماً جاءت من الخارج إلى ملفّ دفعي.
القرار العملي بسيط. إن كان الهدف exe فلا تتوسّط cmd.exe. إن لم يكن لك خيار سوى استدعاء .bat فالمبدأ ألّا تدع الدفعة تفسّر قيماً جاءت من الخارج. اكتب القيم في ملفّ، واجعل الدفعة تمرّر مسار ذلك الملفّ فقط، كسلسلة ثابتة، إلى الـ exe اللاحق، واقرأ محتويات الملفّ في جهة الـ exe. وضع القيمة في متغيّر بيئة ليس حدوداً، لأنّه في اللحظة التي توسّعها الدفعة كـ %VAR% يعيد cmd.exe تفسير & و|. المرور عبر متغيّر بيئة مقبول فقط عندما يقرأ الـ exe اللاحق المتغيّر مباشرة دون المرور بالدفعة. إن تعسّر ذلك أيضاً فانقل محتويات الدفعة إلى PowerShell أو exe خاصّ بك («هل ينبغي نقل ذلك الملفّ الدفعي إلى PowerShell؟»).
10. حدود الطول
تختلف الحدود أيضاً حسب المسار.
| المسار | الحدّ | المصدر |
|---|---|---|
lpCommandLine لـ CreateProcess |
32,767 وحدة ترميز UTF-16 (بما فيها الـ null الختامي؛ الزوج البديل يُعدّ اثنتين) | 1 |
جزء اسم الملفّ التنفيذي عندما يكون lpApplicationName هو NULL |
MAX_PATH |
1 |
| سطر أوامر cmd.exe (بما فيه الأسطر في ملفّ دفعي) | 8,191 حرفاً | 8 |
ProcessStartInfo.Arguments في .NET |
طول السلسلة (وحدات ترميز UTF-16) أقلّ من 32,699 | 16 |
تصميم يصفّ قيماً متغيّرة الطول مثل قائمة ملفّات وسائط يصطدم بالحدّ يوم يزيد العدد. للاستخدامات التي تقترب من الحدّ انتقل إلى طريقة «ملفّ الاستجابة»: اكتب الوسائط في ملفّ واحد ومرّر مسار ذلك الملفّ فقط. الحلّ الرسمي لحدّ cmd.exe هو الطريقة نفسها.8 لكن لا CreateProcess ولا cmd.exe يوسّعان الملفّ عنك. تعمل هذه الطريقة فقط إن استطاع البرنامج المستهدف قراءة ملفّ استجابة بنحو مثل @file، أو إن أمكنك إصلاح الهدف ليقدر. إن كان الهدف exe جاهزاً لا تستطيع تعديله، الخيار الوحيد تقسيم الاستدعاءات بحيث يستوعب كلّ منها الحدّ.
flowchart TB
accTitle: حدود تمرير قيم متغيّرة الطول وسائط، والطريق حولها
accDescr: تصفيف قيم متغيّرة الطول مثل قائمة ملفّات وسائط يبلغ حدّ cmd.exe البالغ 8191 حرفاً أو حدّ CreateProcess البالغ 32767 وحدة ترميز UTF-16 مع نموّ العدد. إن استطاع الهدف قراءة ملفّ استجابة (أو أمكن إصلاحه) فانتقل إلى طريقة ملفّ الاستجابة بكتابة القيم في ملفّ وتمرير المسار فقط؛ إن كان الهدف exe جاهزاً لا يقدر فقسّم الاستدعاءات
list["صفّ قيماً متغيّرة الطول (قائمة ملفّات إلخ) وسائط"] --> grow["تنمو السلسلة مع نموّ العدد"]
grow --> lim["يُبلَغ الحدّ (cmd.exe 8,191 / CreateProcess 32,767)"]
lim --> fail["في يوم يفشل التشغيل فجأة"]
fail -.->|"يستطيع الهدف قراءة ملفّ استجابة"| resp["اكتب القيم في ملفّ ومرّر المسار فقط (ملفّ استجابة)"]
fail -.->|"exe جاهز لا يقدر"| split["قسّم الاستدعاءات"]
الشكل 13: الحدّ من نوع المشكلات «سليمة اليوم». للوسائط التي تنمو بنسبة العدد، إن استطاع الهدف قراءة ملفّ استجابة (أو أمكن إصلاحه) فافعل ذلك من البداية.
11. تحقّق ممّا وصل فعلاً
قبل زيادة الإحاطة بالتخمين، أقصر طريق النظر إلى الوسائط التي وصلت إلى الهدف. ثلاثة أمور تُنظَر: «السلسلة المجمَّعة في جهة الاستدعاء»، و«السلسلة التي وصلت إلى جهة الهدف»، و«المصفوفة بعد التقسيم»، وأربع وسائل لذلك. قبل ذلك وعد واحد. أيّاً كانت الوسيلة، اخفِ الأسرار قبل تسجيل سطر أوامر في سجلّ. إن وضع التصميم كلمات مرور أو مفاتيح API أو رموزاً في الوسائط، كتابتها كما هي تترك الأسرار في السجلّ، سواء كان سجلّ المستدعي أو سجلّ بدء الهدف. تُحفَظ السجلّات أطول من العملية ويراها أناس أكثر. في الأصل يمكن لعمليات أخرى على الجهاز نفسه قراءة سطر أوامر، كما مع Process Explorer المذكور لاحقاً، لذا التدبير الجذري تصميم يمرّر كلمات المرور والرموز لا وسائط بل عبر مسار آخر مثل الدخل القياسي أو مخزن إعدادات محمي؛ والإخفاء في السجلّات احتياط فوق ذلك. إمّا فسّر الوسائط بعد التقسيم (في جهة الاستدعاء، العناصر قبل التجميع) وأخفِ قيم الخيارات التي قد تكون سرّاً قبل التسجيل، أو فعّل تسجيل السلسلة الخام فقط في وضع تشخيص مقيَّد.
- في جهة الاستدعاء سجّل السلسلة التي جمّعتها. هذا هو
lpCommandLineمباشرة قبل تمريره إلىCreateProcess. تفترض هذه المقارنة تشغيلاً بـUseShellExecute = falseأو استدعاء مباشراً لـCreateProcess. عندما تفتح مستنداً أو عنوان URL بـUseShellExecute = trueيبني ارتباط الملفّ سطر الأوامر الفعلي عبرShellExecuteEx(الفصل 7)، لذا تختلف سلسلة المستدعي وسلسلة الهدف حتّى بلا cmd.exe أو ملفّ دفعي، وليست تلك مشكلة الفصل 9. إن استخدمتArgumentListفي .NET فلا تصلح قائمة العناصر كما هي للمقارنة. العناصر القيم قبل الإحاطة وقبل مضاعفة الـ backslash الختامي، وما يصل إلى نظام التشغيل السلسلة التي نسّقها .NET منها. إمّا أعد بناء سلسلة واحدة من العناصر بالقواعد نفسها لـBuildArgumentsفي الفصل 7 وسجّل ذلك (يعطي النتيجة نفسها للتنسيق الذي يفعلهArgumentListداخلياً)، أو قارن قائمة العناصر مباشرة بالمصفوفة بعد التقسيم. هذه الوسيلة الوحيدة لرؤية «المخزن الأصلي للمستدعي»؛ أمّا Process Explorer وسجلّ الهدف المذكوران أدناه فلا يظهران سوى السلسلة التي أعاد بناءها طور cmd.exe أو دفعة بينهما. عند التسجيل التزم الوعد من المقدّمة وأخفِ قيم العناصر التي قد تكون سرّاً (عنصر مخفي لم يعد يطابق سلسلة الهدف، لذا استثنِ ذلك العنصر من المقارنة). - جهّز exe يعرض وسائطه فقط. شغّله بدل الـ exe المستهدف واجعله يطبع
argsالتي تلقّاها، واحداً في كلّ سطر. إن كتبت القيم كما هي، وسيط يحتوي أسطر جديدة أو أحرف تحكّم قد يظهر أسطر متعدّدة أو يكتب فوق أسطر مجاورة فتُخطئ العدّ، لذا اطبع كلّ قيمة مهروبة كسلسلة JSON مع طولها (الهروب قابل للعكس، لذا يمكن استعادة القيمة الأصلية). لكن تذكّر كما يشرح الفصل 3 أن لشيفرة التقسيم ثلاث سلالات، وأنّها تفسّر صيغاً حافّة مثل علامتَي اقتباس متتاليتين داخل الإحاطة على نحو مختلف. استخدم exe عرض مبنياً بوقت التشغيل نفسه للهدف (C++ بـwmainإن كان الهدف MSVC C/C++، و.NET إن كان .NET). إن كان الهدف برنامجك أنت فأوثق نهج تجاوز exe العرض وتسجيلargvعند بدء الهدف نفسه (وفق قاعدة الإخفاء في البند التالي). لـ .NET تكفي الأسطر القليلة التالية.
using System.Text.Encodings.Web;
using System.Text.Json;
// اهرب الأسطر الجديدة وأحرف التحكّم وعلامات الاقتباس والـ backslash؛ أخرج النصّ الياباني كما هو
var json = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
Console.WriteLine("CommandLine: " + JsonSerializer.Serialize(Environment.CommandLine, json)); // السلسلة الواحدة
for (int i = 0; i < args.Length; i++)
Console.WriteLine($"[{i}] len={args[i].Length} {JsonSerializer.Serialize(args[i], json)}");
// بعد التقسيم. كلّ قيد يسع سطراً واحداً دائماً، والسلسلة الفارغة تظهر len=0 و"". len بوحدات ترميز UTF-16
- انظر إلى سطر أوامر العملية الابن في Process Explorer. تُظهر خصائص العملية سلسلة سطر الأوامر التي تحملها العملية الابن. هذه وسيلة لفحص «السلسلة التي وصلت إلى جهة الهدف»؛ ولا تخبرك «المصفوفة بعد التقسيم». المعروض السلسلة المحفوظة في جهة العملية الابن، لذا كما لُمس في الفصل 2 قد يكون نظام التشغيل قد كمّل المسار الكامل لاسم الملفّ التنفيذي في المقدّمة، وإن توسّط cmd.exe أو ملفّ دفعي فما تراه السلسلة التي أعاد cmd.exe بناءها. النقاط الرئيسة ألّا تفزع من فرق في الرمز في المقدّمة وحده، وأن سلسلة المستدعي الأصلية لا تُعرَف إلّا من السجلّ في البند 1. الاستخدام مشمول في «Process Explorer / Handle / VMMap عملياً».
- عند بدء تطبيقك سجّل سطر الأوامر الذي تلقّاه. عندما يقول أحد في الميدان «لا يبدأ»، وجود سجلّ للسلسلة التي بدأ بها يتيح لك عزل ما إذا كانت مشكلة وسائط أوّلاً. هنا أيضاً لا تحفظ قيمة عودة
GetCommandLineWكما هي. التزم الوعد من المقدّمة: إمّا فسّر الوسائط بعد التقسيم وأخفِ قيماً قد تكون سرّاً قبل التسجيل، أو فعّل تسجيل السلسلة الخام فقط في وضع تشخيص مقيَّد.
ترتيب المقارنة كما يلي. قارن أوّلاً سلسلة المستدعي (البند 1) بسلسلة الهدف (البند 3 أو 4). إن لم تتطابقا بمعزل عن اسم الملفّ التنفيذي في المقدّمة فقد حوّلها طور بينهما: cmd.exe أو ملفّ دفعي لتشغيل مباشر (الفصل 9)، أو ارتباط ملفّ الصدفة لـ UseShellExecute = true (الفصل 7). استبدال شيفرتك بدالّة الفصل 6 لن يصلح ذلك. إن تطابقتا فقارن تلك السلسلة بالمصفوفة بعد التقسيم (البند 2). إن قُسِّمت وفق القواعد لكنّها ليست المصفوفة التي تريد فالمشكلة في جهة التجميع؛ وإن لم تُقسَّم وفق القواعد فالمشكلة محلّل المستقبل.
flowchart TB
accTitle: ترتيب عزل مشكلة وسائط
accDescr: قارن أوّلاً سجلّ السلسلة المجمَّعة في جهة الاستدعاء بسلسلة الهدف كما تُرى في Process Explorer أو سجلّ بدء الهدف. إن لم تتطابقا بمعزل عن اسم الملفّ التنفيذي في المقدّمة فقد حوّلها طور بينهما (cmd.exe أو ملفّ دفعي لتشغيل مباشر، ارتباط ملفّ الصدفة لـ UseShellExecute=true). إن تطابقتا فقارن بالمصفوفة بعد التقسيم؛ إن قُسِّمت بالقواعد لكنّها ليست المصفوفة التي تريد فالمشكلة في جهة التجميع، وإن لم تُقسَّم بالقواعد فالمشكلة محلّل المستقبل
s["الوسائط خاطئة"] --> caller["انظر إلى السلسلة المجمَّعة في جهة الاستدعاء (سجلّ المستدعي)"]
caller --> target["انظر إلى سلسلة الهدف (Process Explorer / سجلّ بدء الهدف)"]
target --> same{"هل تتطابقان بمعزل عن اسم الملفّ التنفيذي في المقدّمة؟"}
same -->|"لا"| mid["حوّلها طور بينهما (انظر الفصلين 9 و7)"]
same -->|"نعم"| arr["انظر إلى المصفوفة بعد التقسيم (exe عرض على وقت التشغيل نفسه للهدف)"]
arr --> cmp{"هل تتقابل السلسلة والمصفوفة بالقواعد؟"}
cmp -->|"نعم: قُسِّمت، لكن ليست المصفوفة التي تريد"| build["مشكلة جهة تجميع: استبدل بدالّة الفصل 6"]
cmp -->|"لا: لم تُقسَّم بالقواعد"| recv["مشكلة في محلّل المستقبل"]
الشكل 14: قارن الأمور الثلاثة بالترتيب، «سلسلة المستدعي» و«سلسلة الهدف» و«المصفوفة»، فيُقرَّر آلياً ما إذا كانت المسؤولية تقع على طور بينهما أو جهة التجميع أو جهة الاستقبال. زيادة الهروب بالتخمين يمكن أن تنتظر حتّى بعد هذا الفحص.
12. دليل تقريبي (جدول قرار)
| الوضع | ما تفعله |
|---|---|
| تشغيل exe من .NET Core 2.1 فما بعده / .NET 5 فما بعده | أضف إلى ProcessStartInfo.ArgumentList عنصراً عنصراً |
| تشغيل exe من .NET Framework | جمّع Arguments بدالّة تتبع قواعد الفصل 6. لا تكتب علامات اقتباس يدوياً |
| التشغيل من C++ | مرّر lpApplicationName، وجمّع lpCommandLine بالقواعد في مخزن قابل للكتابة |
| تريد علامة اقتباس داخل قيمة وسيط | استخدم صيغة \" فقط. لا تضع علامات اقتباس متجاورة داخل وسيط غير فارغ |
| ينتهي المسار بـ backslash | إن أحطت فضاعف الـ backslash الختامي. إن لم توجد مسافات فلا تُحِط |
| تريد تمرير وسيط فارغ | ضع "". إن أغفلتها اختفى الوسيط كلّه |
| يحتوي مسار الملفّ التنفيذي مسافة | مرّر lpApplicationName، وأحِط الرمز في المقدّمة بعلامات اقتباس أيضاً |
لا خيار سوى استدعاء .bat |
لا تدع الدفعة تفسّر قيماً جاءت من الخارج. اكتبها في ملفّ واجعل الـ exe اللاحق يقرأه (متغيّر بيئة يُوسَّع كـ %VAR% داخل الدفعة ليس حدوداً) |
| تطول الوسائط | إن استطاع الهدف قراءة ملفّ استجابة (أو أمكن إصلاحه) فانتقل إلى ملفّ استجابة. لـ exe جاهز قسّم الاستدعاءات |
| لا تعرف ما يصل | قارن الثلاثة بالترتيب: سجلّ المستدعي، وسلسلة الهدف (Process Explorer / سجلّ البدء)، والمصفوفة بعد التقسيم (exe عرض على وقت التشغيل نفسه للهدف) |
13. خلاصة
تعبر وسائط سطر أوامر Windows الحدود لا كمصفوفة بل كـ سلسلة واحدة. يربط المستدعي، ويقسّم المستقبل، وتُختزَل قواعد التقسيم إلى ثلاث: «قسّم على المسافات»، و«أحِط بعلامات اقتباس»، و«فقط الـ backslash مباشرة قبل علامة اقتباس خاصّ». يتبع اسم الملفّ التنفيذي في المقدّمة وحده قاعدة مختلفة، وإغفال lpApplicationName يجعل تفسير مسار يحتوي مسافات غامضاً.
ما على جهة التجميع فعله يسع دالّة واحدة، وعلى .NET Core 2.1 فما بعده يتولّاه ArgumentList. للملفّ التنفيذي مرّر المسار الكامل في lpApplicationName وضع أيضاً المسار نفسه، محاطاً بعلامات اقتباس، في بداية lpCommandLine (في .NET أَوكِله إلى FileName). لا تولّد صيغة علامات اقتباس متجاورة داخل وسيط غير فارغ ("" التي تمثّل وسيطاً فارغاً أمر آخر)، ولا تمرّر قيماً جاءت من الخارج عبر cmd.exe أو ملفّ دفعي، وللوسائط التي تنمو بنسبة العدد استخدم ملفّ استجابة فقط عندما يستطيع الهدف قراءة واحد (أو يمكن إصلاحه ليقرأ واحداً)، وإلّا فقسّم الاستدعاءات. التزم هذه النقاط الخمس فلا تحدث أعطال «لا يبدأ إلّا على جهاز فيه مسافة في المسار» و«يختفي الوسيط التالي بسبب backslash ختامي».
flowchart TB
accTitle: خمسة وعود تمنع أعطال الوسائط
accDescr: مرّر المسار الكامل للملفّ التنفيذي في lpApplicationName وأحِط الرمز في المقدّمة بعلامات اقتباس أيضاً، وأَوكِل الإحاطة إلى دالّة تتبع القواعد أو إلى ArgumentList، ولا تولّد صيغة علامات اقتباس متجاورة داخل وسيط غير فارغ، ولا تمرّر قيماً من الخارج عبر cmd.exe أو ملفّ دفعي، واستخدم ملفّ استجابة للوسائط التي تنمو مع العدد فقط عندما يستطيع الهدف قراءة واحد. على فرض أن الهدف يفسّر بقواعد التقسيم المنشورة ولم يفعّل توسيع أحرف البدل، تمنع هذه النقاط الخمس الأعطال الناجمة عن مسارات فيها مسافات وbackslash ختامي
r0["مرّر المسار الكامل في lpApplicationName وأحِط الرمز في المقدّمة أيضاً"]
r1["أَوكِل الإحاطة إلى دالّة تتبع القواعد أو إلى ArgumentList"]
r2["لا تولّد علامات اقتباس متجاورة داخل الإحاطة"]
r3["لا تمرّر قيماً من الخارج عبر cmd.exe أو ملفّ دفعي"]
r4["استخدم ملفّ استجابة للوسائط النامية (عندما يستطيع الهدف القراءة)"]
goal["لا أعطال من المسافات أو الـ backslash الختامي"]
r0 ~~~ r1 ~~~ r2 ~~~ r3 ~~~ r4
r0 --> goal
r1 --> goal
r2 --> goal
r3 --> goal
r4 --> goal
الشكل 15: كلّ من الوعود الخمسة إعادة صياغة لـ «ثبّت الوحدة المراد تنفيذها، ومرّر فقط سلاسل يستطيع محلّل الهدف تقسيمها». الفرضية أن الهدف يفسّر بقواعد التقسيم المنشورة ولم يفعّل توسيع أحرف البدل (الفصلان 6 و8)؛ وفوق ذلك تمنع هذه النقاط الخمس الأعطال الناجمة عن المسافات والـ backslash الختامي.
عندما لا يعمل الأمر، قبل زيادة الهروب بالتخمين، انظر إلى الأمور الثلاثة: السلسلة المجمَّعة في جهة الاستدعاء، والسلسلة التي وصلت إلى جهة الهدف، والمصفوفة بعد التقسيم. إن اختلفت سلسلتا المستدعي والهدف فالمسؤول طور بينهما (cmd.exe أو ملفّ دفعي، أو ارتباط ملفّ الصدفة لـ UseShellExecute = true)؛ وإن كانتا سواء فتقابل السلسلة والمصفوفة يقرّر ما إذا كانت جهة التجميع أو جهة الاستقبال.
مقالات ذات صلة
- استدعاء exe خارجي صحيحاً من PowerShell ── مطبّات إحاطة الوسائط ورموز الخروج والتخبيص
- قائمة تحقّق للتعامل الآمن مع العمليات الابنة في تطبيقات Windows ── أفضل ممارسات كائنات Job وانتشار الخروج والإدخال/الإخراج القياسي وwatchdog
- ما الذي يبقى بعد موت الأب ── تربية العمليات الابنة في كائن Job
- MAX_PATH ومطبّات المسارات وأسماء الملفّات في Windows ── حدّ 260 حرفاً، والأسماء المحجوزة، والنقطة الختامية، وحساسيّة الأحرف
- تكامل صدفة Windows اليوم ── قوائم السياق وارتباطات الملفّ وما تغيّر في Windows 11
- منع التشغيل المتعدّد لتطبيق Windows ── mutex مسمّى وتنشيط النافذة القائمة عند تشغيل ثانٍ
- استدعاء Win32 API بأمان من C# ── دليل P/Invoke العملي (DllImport / LibraryImport / CsWin32)
- هل ينبغي نقل ذلك الملفّ الدفعي إلى PowerShell؟ ── جرد أصول cmd/bat وقرار الترحيل
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع تصميم تطبيقات Windows التي تجمع أدوات خارجية وEXE داخلية، وتحقيق السبب الجذري لتشغيل عمليات ابنة «تبدأ في بعض البيئات ولا تبدأ في أخرى»، ومراجعة شيفرة تشغيل العمليات ضمن الترحيل من .NET Framework إلى .NET. نرحّب بالاستشارة حتّى من حالة واحدة لـ «تتشوّه الوسائط».
- تطوير تطبيقات Windows
- تحقيق الأعطال وتحليل الأسباب
- ترحيل الأصول القائمة وإعادة استخدامها
- الاستشارة التقنيّة ومراجعة التصميم
- التواصل معنا
روابط مرجعيّة
-
Microsoft Learn, CreateProcessW function (processthreadsapi.h). حول كون
lpCommandLineسلسلة واحدة أقصاها 32,767 حرفاً (بما فيها الـ null الختامي؛ وحدات ترميز UTF-16 لأنّها سلسلة عريضة)، وإمكان إصدار Unicode تعديل محتوياتها لذا لا يمكن تمرير ذاكرة للقراءة فقط، وصيرورة الرمز المفصول بمسافات في المقدّمة اسم الوحدة عندما يكونlpApplicationNameهوNULLمع تفسير مسار يحتوي مسافات بدءاً منc:\program.exe، وخطر تشغيل ملفّ تنفيذي آخر إن وُضعProgram.exeهناك وضرورة تجنّبNULLأو إحاطة المسار بعلامات اقتباس، وإمكان عدم تطابقargv[0]مع اسم الوحدة عندما يُحدَّد الاثنان، وتقييد جزء اسم الوحدة بـMAX_PATHعندما يكونNULL، وضرورة cmd.exe /c لتشغيل ملفّ دفعي. انظر أيضاً الملاحظة في CreateProcessA function بأن فريق هندسة MSRC لا يوصي بهذه الطريقة (مع رابط إلى شرح MS14-019). ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
Microsoft Learn, GetCommandLineW function (processenv.h). حول إعادتها سلسلة سطر أوامر العملية الحالية، وعدم تحرير قيمة العودة أو تعديلها، وإمكان تحويلها إلى صيغة argv عبر
CommandLineToArgvW، وإمكان عدم تطابقها مع السلسلة التي مرّرها الأب إلىCreateProcessلأن نظام التشغيل يكمّل المسار الكامل لاسم الملفّ التنفيذي. ↩ ↩2 -
Microsoft Learn, CommandLineToArgvW function (shellapi.h). حول المعاملة الخاصّة للـ backslash مباشرة قبل علامة اقتباس مزدوجة (2n تعطي n مع فتح الإحاطة أو إغلاقها، و2n+1 تعطي n مع علامة اقتباس حرفية، وتبقى كما هي عندما لا تلي علامة اقتباس)، وصيرورة المسافات جزءاً من الوسيط في وضع «داخل علامات الاقتباس»، وجواز اسم البرنامج في المقدّمة بعلامات اقتباس أو بدونها، وصيرورة الوسيط الأوّل سلسلة فارغة عندما يبدأ
lpCmdLineبمسافة، وإعادة مسار الملفّ التنفيذي الحالي عندما تُمرَّر سلسلة فارغة، وتحرير قيمة العودة بـLocalFreeواحد. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn,
mainfunction and command-line arguments. حول القواعد التي يفسّر بها رمز بدء Microsoft C/C++ سطر الأوامر (الفصل بمسافات وعلامات جدولة، وإمكان إحاطةargv[0]بعلامات اقتباس لكن القواعد التالية لا تسري عليه، وكون السلسلة المحاطة وسيطاً واحداً، وكون علامة الإقحام ليست حرف هروب، وكون علامتَي اقتباس متتاليتين داخل علامات الاقتباس علامة واحدة، وكون كلّ شيء إلى النهاية الوسيط الأخير عندما لا توجد علامة إغلاق، ومعاملة الأعداد الزوجية والفردية من الـ backslash)، وجدول الدخل وargv، وتوسيع أحرف البدل بـsetargv.obj، وإمكان ألّا يكونargv[0]اسم الملفّ التنفيذي عندما يُحدَّدlpApplicationNameوlpCommandLineمعاً لذا ينبغي الحصول عليه بـGetModuleFileName. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, ProcessStartInfo.ArgumentList Property. حول عدم حاجة السلاسل المضافة إلى هروب مسبقاً، واستقلال
ArgumentListوArgumentsوعدم إمكان استخدامهما معاً، وهروبArgumentListللوسائط وتجميعه سلسلة واحدة داخلياً تُمرَّر إلى نظام التشغيل عندProcess.Start، وكونArgumentListالاختيار إن لم تكن واثقاً من الإحاطة، وخطر جمعه مع بيانات غير موثوقة، وسريانه على .NET Core 2.1 فما بعده. ↩ ↩2 ↩3 ↩4 -
dotnet/runtime (GitHub), PasteArguments.cs وPasteArguments.Windows.cs. شيفرة التجميع المستخدمة داخل
ArgumentList. حول وضع وسيط غير فارغ بلا مسافات ولا علامات اقتباس كما هو، وإلّا إحاطته بعلامات اقتباس، ومضاعفة الـ backslash الختامي، وجعل الـ backslash قبل علامة اقتباس مضاعفاً زائداً واحداً، وتقديم علامات الاقتباس دائماً بـ backslash، وعدم توليد صيغة علامة اقتباس تلي علامة إغلاق لأن VC قبل 2008 وبعده يفسّرانها على نحو مختلف، ولـargv[0]الإحاطة بعلامات اقتباس فقط إن وُجدت مسافات والإلقاء إن احتوى علامة اقتباس. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, about_Parsing. حول وصول وسائط الملفّ الدفعي إلى cmd.exe كسلسلة سطر أوامر خام، والتحذير من تمرير دخل غير موثوق. ↩ ↩2
-
Microsoft Learn, Command prompt (Cmd.exe) command-line string limitation. حول كون أقصى طول سلسلة في موجه الأوامر 8,191 حرفاً، وسريانه على أسطر الأوامر داخل ملفّ دفعي أيضاً، وطريقة التحايل بكتابة الوسائط في ملفّ وتمرير اسم ذلك الملفّ. ↩ ↩2 ↩3
-
Microsoft Learn, WinMain function (winbase.h). حول كون
lpCmdLineسطر الأوامر بلا اسم البرنامج، والحصول على سطر الأوامر كلّه بـGetCommandLine، ووجودwWinMainكنقطة دخول Unicode. ↩ -
dotnet/runtime (GitHub), apphost.c وdotnet.cpp. حول كون نقطتَي دخول apphost و
dotnet.exeهماwmain(int argc, wchar_t* argv[])على Windows وتمريرargvالذي بناه وقت تشغيل C مباشرة إلى معالجة بدء المضيف. ↩ -
dotnet/runtime (GitHub), corhost.cpp. حول بناء
ExecuteAssemblyمصفوفةEnvironment.GetCommandLineArgs()بـSetCommandLineArgs(pwzAssemblyPath, argc, argv)، وكون العنصر الأوّل اسم التشغيل الممرَّر من المضيف (أو مسار التجميع إن لم يوجد) ويليهargv، ووصول ذلكargvفقط إلىMain. ↩ ↩2 -
dotnet/runtime (GitHub), Environment.cs وEnvironment.Windows.cs. حول إعادة
GetCommandLineArgsالمصفوفة المهيَّأة عند البدء (s_commandLineArgs)، وسقوط مكتبة مستضافة بلا ذلك إلى تقسيم قيمة عودةGetCommandLineWبـSegmentCommandLineالخاص بوقت التشغيل، واتّباع تلك القواعد وثائق دالّةmainفي MSVC، وعدم استخدامCommandLineToArgvWلأن سلوكه يختلف قليلاً. ↩ ↩2 ↩3 -
Microsoft Learn, Main() and command-line arguments. حول عدم صيرورة
argsفيMainnull أبداً، وخلافاً لـ C/C++ عدم تضمين اسم البرنامج في بدايةargsبل كونه العنصر الأوّل لـGetCommandLineArgs(). ↩ -
Microsoft Learn, dotnet command. حول كون تشغيل التطبيق بالشكل
dotnet [خيارات وقت التشغيل] <مسار التطبيق> [وسائط]، والوسائط بعد مسار التطبيق هي ما يصل إلى التطبيق. ↩ -
Microsoft Learn, Environment.GetCommandLineArgs Method. حول كون العنصر الأوّل اسم الملفّ التنفيذي، وفصل الوسائط بمسافات مع سماح علامات الاقتباس المزدوجة بمسافات داخلها، وعدم امتلاك علامات الاقتباس المفردة تلك الوظيفة، وقواعد الأعداد الزوجية والفردية من الـ backslash وعلامات الاقتباس، وجدول الدخل والنتائج. ↩
-
Microsoft Learn, ProcessStartInfo.Arguments Property. حول كون طول السلسلة أقلّ من 32,699، وتفسير التطبيق المستهدف للوسائط لذا يجب أن تطابق توقّعاته، وعدم تمرير علامات الاقتباس نفسها إلى الهدف عندما يُحاط وسيط يحتوي مسافات، واستقلاله عن
ArgumentList. ↩ ↩2 -
Microsoft Learn, cmd. حول كون
&و|و( )أحرفاً خاصّة تحتاج^أو علامات اقتباس، وقائمة الأحرف الخاصّة التي ينبغي إحاطتها، وشروط الاحتفاظ بعلامات الاقتباس عند تحديد/c/k(عدم استخدام/s، وزوج واحد من علامات الاقتباس، وعدم احتواء أحرف خاصّة، واحتواء مسافات، وكونه اسم ملفّ تنفيذي) وطريقة نزع علامة الاقتباس الأولى عندما لا تُستوفى الشروط. ↩ -
Microsoft Security Response Center, MS14-019 – Fixing a binary hijacking via .cmd or .bat file وMicrosoft Security Bulletin MS14-019. حول إمكان اختطاف cmd.exe عندما مُرِّر .cmd / .bat مباشرة إلى
CreateProcessلأنّه بُحث عنه في الدليل الحالي أوّلاً، واستخدام cmd.exe النظام دائماً بعد الإصلاح، وتوصية أن تمرّر التطبيقات المسار المؤهَّل بالكامل لـ cmd.exe وتجعل الدفعة وسيطه. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
ما الذي يبقى بعد موت الأب ── تربية العمليات الابنة في كائن Job
لماذا يبقى مساعد SDK ماسكاً الكاميرا أو منفذ COM بعد إنهاء الواجهة قسراً. كيف تجعل كائن Job شجرة العمليات وحدة واحدة وتصمم عمر العملية ال...
الأنابيب المسمّاة عمليّاً ── IPC القياسيّ في Windows من التصميم إلى الأمان
دليل عمليّ للأنابيب المسمّاة، وسيلة IPC القياسيّة في Windows. يغطّي وضع البايت مقابل وضع الرسالة، وخوادم متعدّدة العملاء، وأمان ACL وانتح...
Time Travel Debugging ── تسجيل الأخطاء التي لا تتكرّر في التطبيقات طويلة التشغيل وإرجاعها
خطأ يظهر مرّة في الشهر لا يترك في تفريغ الانهيار سوى نتيجته. سجّل التنفيذ وأرجعه بـ WinDbg Time Travel Debugging (TTD): TTD.exe والمخزن ا...
واجهة مجمع مؤشّرات الترابط Win32 ── تزامن دون إنشاء مؤشّرات ترابط، عبر CreateThreadpoolWork
أتستدعي CreateThread في كلّ مكان في الشيفرة الأصليّة؟ دليل من المصادر الأوّليّة لواجهة مجمع مؤشّرات الترابط Win32: كائنات work وtimer وwa...
DllMain وقفل المحمِّل ── السبب الحقيقي لعبارة «لا تفعل شيئاً في تهيئة الـDLL»
لماذا يُحظَر استدعاء LoadLibrary أو مزامنة مؤشّرات الترابط من DllMain. نشرح من المصادر الأوّليّة كيف يسلسل قفل المحمِّل كلّ إشعار DLL، وس...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- ألا توجد في Windows واجهة تمرّر مصفوفة وسائط؟
- لا. ما يتلقّاه CreateProcess سلسلة واحدة تُسمَّى lpCommandLine، وهذه السلسلة هي ما يصل إلى العملية الجديدة (قد يكمّل نظام التشغيل المسار الكامل لاسم الملفّ التنفيذي في المقدّمة فقط). ما يبدو مصفوفة argv يُنشأ داخل العملية المستقبلة برمز بدء وقت تشغيل C، أو CommandLineToArgvW، أو وقت تشغيل .NET وهو يقسّم السلسلة. إذن «تمرير الوسائط» هو نفسه بناء سلسلة سيقسّمها محلّل الطرف الآخر إلى القطع الأصلية.
- متى يصبح الـ backslash حرف هروب؟
- فقط عندما تليه علامة اقتباس مزدوجة مباشرة. الـ backslash الذي لا تليه علامة اقتباس مزدوجة يبقى كما هو، مهما بلغ عدد المتتالي منها. إن سبق علامة الاقتباس المزدوجة 2n من الـ backslash صارت n من الـ backslash وفُتحت الإحاطة أو أُغلقت؛ وإن سبقها 2n+1 صارت n من الـ backslash وعلامة اقتباس حرفية. بسبب هذا اللاتناظر يجب مضاعفة الـ backslash الختامي في المسار فقط عندما تحيط المسار بعلامات اقتباس.
- أيّهما أستخدم، ProcessStartInfo.ArgumentList أم Arguments؟
- إن جاءت القيم من متغيّرات فـ ArgumentList. يصير العنصر الواحد وسيطاً واحداً، ويطبّق .NET الإحاطة والهروب اللازمين، ويجمّع سلسلة واحدة داخلياً قبل تسليمها إلى نظام التشغيل. Arguments خاصّية تمرّر سلسلة بنيتها بنفسك كما هي؛ والاثنتان مستقلّتان ولا يمكن استخدامهما معاً. لكن ArgumentList واجهة ظهرت في .NET Core 2.1 ولا توجد في .NET Framework. على .NET Framework ابنِ Arguments بدالّة التجميع في هذه المقالة.
- هل يمكن كتابة علامتَي اقتباس متجاورتين داخل وسيط محاط بعلامات اقتباس؟
- لا تولّدها في جهة التجميع، لأن المستقبلين يختلفون في تفسيرها. المقصود هنا إحاطة وسيط غير فارغ بعلامات اقتباس ووضع علامتَي اقتباس متجاورتين داخله. أمّا "" التي تمثّل وسيطاً فارغاً (علامتا اقتباس فقط) فأمر آخر، وهي الطريقة الصحيحة لتمرير سلسلة فارغة. وفق قواعد وقت تشغيل C في MSVC تُعامَل علامتَا اقتباس متتاليتان داخل سلسلة محاطة كعلامة واحدة، لكن القواعد الرسمية لـ CommandLineToArgvW لا تصف هذه المعاملة، ويصرّح مصدر وقت تشغيل .NET بأنّه لا يولّد هذه الصيغة لأن VC قبل 2008 وبعده يفسّرانها على نحو مختلف. عندما تريد تمرير علامة اقتباس حرفاً، ضع backslash أمامها، فيعطي كلّ محلّل النتيجة نفسها.
- عندما يحتوي مسار الملفّ التنفيذي مسافة، ماذا أمرّر إلى CreateProcess لأكون في أمان؟
- الطريقة الموثوقة تمرير المسار الكامل للملفّ التنفيذي في lpApplicationName ووضع المسار نفسه، محاطاً بعلامات اقتباس، في بداية lpCommandLine أيضاً. إن كان lpApplicationName هو NULL خمّن CreateProcess اسم الملفّ التنفيذي من بداية lpCommandLine، مقسّماً على المسافات. للسلسلة C:\Program Files\MyApp -L -S يختبر أوّلاً وجود C:\Program.exe، فإن وُجد ملفّ خبيث هناك جرى ذلك. تنصّ الوثائق الرسمية على هذا الخطر صراحة وتطلب تجنّب NULL أو إحاطة المسار بعلامات اقتباس.
- هل تنطبق القواعد نفسها عند تمرير وسائط إلى ملفّ دفعي؟
- لا. يفسّر cmd.exe الملفّ الدفعي، ويعامل cmd.exe سطر الأوامر سلسلة خام بلا تقسيم إلى وسائط. تعمل رموز مثل & و| والأقواس و^ نحواً لـ cmd.exe، لذا لا تجعل الإحاطة بقواعد CommandLineToArgvW الأمر آمناً. تحذّر الوثائق الرسمية من تمرير دخل غير موثوق إلى ملفّات دفعية. اكتب القيم في ملفّ واجعل الـ exe اللاحق، لا الدفعة، يقرأه، أو انقل محتويات الدفعة إلى PowerShell أو exe خاصّ بك. وضع القيمة في متغيّر بيئة ليس حدوداً أيضاً، لأنّه ما إن توسّعها الدفعة كـ %VAR% حتّى يعيد cmd.exe تفسير الرموز.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.