لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows

· آخر تحديث: · · Windows, تطوير Windows, C#, C++, Win32 API, .NET, عملية

سجل التعديلات (النسخة الأولى، نُشرت في 2 Sep، 2026)
النشر الأول

«نجحت في الاختبار، لكن الأداة الخارجية لا تبدأ على جهاز يحتوي مساره مسافة». «مرّرت C:\data\ فاندمجت مع الوسيط التالي في واحد». «مرّرت JSON وسيطاً فاختفت علامات الاقتباس وفشل الطرف الآخر في التحليل» ── أعطال تتكرّر في الشيفرة التي تشغّل عمليات ابنة. معظمها لا يأتي من المنطق بل من كتابة تفترض غير ما هو قائم: ليس في Windows آلية لتمرير «مصفوفة وسائط».

ما يتلقّاه CreateProcess، الدالّة التي تنشئ عملية على Windows، هو سلسلة واحدة تُسمَّى lpCommandLine. مهما أعدّ المستدعي المصفوفة بعناية، تُربَط دائماً في سلسلة واحدة عند عبور حدود نظام التشغيل، ويعيد الطرف المستقبل تقسيمها. يقرّر وقت تشغيل الطرف المستقبل قواعد التقسيم، ووقت تشغيل C وCommandLineToArgvW ووقت تشغيل .NET وcmd.exe شيفرات منفصلة. تمرير الوسائط يعني بناء سلسلة سيقسّمها محلّل الطرف الآخر إلى القطع الأصلية.

تتّخذ هذه المقالة موقف تشغيل عمليات ابنة من شيفرة Win32 و.NET، لا من سكربتات PowerShell، وترتب أين تُربَط السلسلة وأين تُقسَّم وأيّ القواعد تسري. جهة PowerShell (تغيير تمرير الوسائط في 7.3، و--%، و$PSNativeCommandArgumentPassing) مشمولة في «استدعاء exe خارجي صحيحاً من PowerShell»، لذا تحفر هذه المقالة الطبقة تحت ذلك.

الطبقة التي تغطّيها هذه المقالةتمرير وسائط PowerShell مشمول في مقالة أخرى؛ تغطّي هذه المقالة الطبقة تحت ذلك، من CreateProcess في Win32 وProcessStartInfo في .NET حتّى محلّل الـ exe المستهدفنطاق هذه المقالةتمرير وسائط PowerShell (مقالة أخرى)ProcessStartInfo في .NETCreateProcessW في Win32سلسلة سطر أوامر واحدةمحلّل الـ exe المستهدف

الشكل 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

المسار الذي تسلكه الوسائط حتّى تصل إلى العملية الابنتُربَط مصفوفة وسائط المستدعي في سلسلة واحدة في lpCommandLine لـ CreateProcess وتُمرَّر إلى العملية الجديدة، وتقسّم العملية الابن السلسلة التي تستخرجها بـ GetCommandLineW بمحلّلها الخاص لتنشئ argvمصفوفة وسائط المستدعيتُربَط في سلسلة واحدة (مسؤولية المستدعي)lpCommandLine لـ CreateProcessWمعاملات العملية الجديدةالسلسلة التي يعيدها GetCommandLineWمحلّل الطرف المستقبل يقسّمهامصفوفة 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 تظهر الفروق عند الحواف، مثل معاملة "" المذكورة لاحقاً، ونادراً ما تصيبها الوسائط اليومية، لكن افتراض أن «القواعد واحدة فكلّ شيء يمرّ» هو ما ينكسر عند الحواف.

المحلّلات الثلاثة في الطرف المستقبلالسلسلة الواحدة التي يعيدها GetCommandLineW يقسّمها رمز بدء وقت تشغيل C لـ C/C++، وCommandLineToArgvW للاستخدام المباشر لـ Win32، وشيفرة التقسيم الخاصّة بوقت التشغيل لـ .NET المحمَّل مكتبة مستضافة؛ يتبع كلّ منها قواعد بالهيكل نفسه لكنّه تنفيذ منفصل. يتلقّى تطبيق .NET معتاد يُشغَّل عبر apphost أو dotnet.exe المصفوفة التي قسّمها رمز بدء وقت تشغيل C للمضيفسلسلة GetCommandLineWرمز بدء وقت تشغيل CCommandLineToArgvWشيفرة التقسيم الخاصّة بـ .NET (عند التحميل بمضيف).NET عبر apphost / dotnet.exe هو نفسههيكل القواعد نفسه، تنفيذات منفصلة

الشكل 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

  1. تُفصَل الوسائط بـ مسافات أو علامات جدولة.
  2. يصير النطاق المحاط بـ علامتي اقتباس مزدوجتين وسيطاً واحداً حتّى إن احتوى مسافات. علامات الاقتباس نفسها ليست جزءاً من الوسيط. قد تبدأ علامة الاقتباس في وسط وسيط، وإن انتهت السلسلة بلا علامة إغلاق صار كلّ ما إلى النهاية الوسيط الأخير.
  3. يُعامَل الـ backslash حرفاً عادياً. فقط عندما تليه علامة اقتباس مزدوجة مباشرة تسري القواعد التالية.
  4. إن سبق علامة اقتباس مزدوجة 2n من الـ backslash، خرج n من الـ backslash، وعملت علامة الاقتباس «بداية الإحاطة أو نهايتها».
  5. إن سبق علامة اقتباس مزدوجة 2n+1 من الـ backslash، خرج n من الـ backslash وعلامة اقتباس حرفية، ولم تتغيّر حالة الإحاطة.
  6. علامة الإقحام (^) ليست حرف هروب (تلك قاعدة cmd.exe، لا قاعدة المحلّل).

يحفظ المحلّل بتّة حالة واحدة، «هل أنا داخل علامات اقتباس»، ويقلبها عند كلّ علامة اقتباس، ويقرأ السلسلة من اليسار إلى اليمين. ما إذا كانت المسافة تفصل الوسائط يُقرَّر بهذه الحالة.

تدفّق التقسيم الذي يتبدّل بين داخل علامات الاقتباس وخارجهاخارج علامات الاقتباس يفصل المحلّل الوسائط على المسافات؛ وعندما يلقى علامة اقتباس يدخل ويُعامل المسافات جزءاً من الوسيط؛ وعندما يلقى علامة أخرى يعود إلى الخارج. يُعامَل الـ backslash معاملة خاصّة فقط عندما تليه علامة اقتباس مباشرةتُلقى علامة اقتباستُلقى علامة اقتباسيلي الـ backslash علامة اقتباس مباشرةيلي الـ backslash علامة اقتباس مباشرة2n: أخرج n وافتح/أغلق2n+1: أخرج n وعلامة اقتباس حرفيةخارج علامات الاقتباس: التقسيم على المسافاتداخل علامات الاقتباس: المسافات جزء من الوسيطتطبيق قاعدة الـ backslashاقلب حالة الإحاطةأبقِ حالة الإحاطة

الشكل 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 بعلامات اقتباس، تتحوّل علامة الإغلاق إلى حرف ولا تُغلق الإحاطة أبداً.

كيف يبتلع الـ backslash الختامي الوسيط التاليعندما يُحاط مسار ينتهي بـ backslash بعلامات اقتباس، تجلس علامة الاقتباس التي كان ينبغي أن تغلق مباشرة بعد backslash واحد فتُفسَّر علامة اقتباس حرفية، فلا تُغلق الإحاطة ويُقرأ كلّ شيء حتّى الوسيط التالي وسيطاً واحداًضاعف الـ backslashمسار محاط ينتهي بـ backslash واحدعدد فردي من الـ backslash يسبق علامة الإغلاقتُخرَج علامة الاقتباس حرفاً ولا تُغلق الإحاطةالمسافات اللاحقة لم تعد تفصليصل كلّ شيء حتّى الوسيط التالي وسيطاً واحداًتُغلق الإحاطة وتنفصل الوسائط

الشكل 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

ترتيب تخمين الملفّ التنفيذي عندما يكون lpApplicationName هو NULLإن مُرِّر C:\Program Files\MyApp -L -S بلا علامات اقتباس، يختبر CreateProcess وجود C:\Program.exe ثم C:\Program Files\MyApp.exe بذلك الترتيب، فإن وُضع C:\Program.exe هناك جرى ذلكموجودغير موجودمرّر lpApplicationName، أو أحِط الرمز في المقدّمةتمرير مسار بلا علامات اقتباس (يحتوي مسافات) في lpCommandLineالمرشّح 1: جرّب C:\Program.exeيبدأ ملفّ تنفيذي غير مقصودالمرشّح 2: جرّب C:\Program Files\MyApp.exeيبدأ الملفّ التنفيذي المقصود

الشكل 6: وضع مسار يحتوي مسافات في البداية بلا علامات اقتباس يجعل نظام التشغيل يجرّب المرشّحات من الأقصر. تسمّي الوثائق الرسمية هذا «خطراً» بعبارات صريحة.

تنصّ الوثائق الرسمية على أنّه إن وُضع C:\Program.exe هناك جرى بدل التطبيق المقصود، وتطلب ألّا تمرّر NULL لـ lpApplicationName، وإن فعلت فأحِط المسار في المقدّمة بعلامات اقتباس.1 عملياً افعل الاثنين. مرّر المسار الكامل للملفّ التنفيذي في lpApplicationName، وضع أيضاً المسار نفسه، محاطاً بعلامات اقتباس، في بداية lpCommandLine. عندما يُمرَّر الاثنان تُقرَّر الوحدة التي تعمل بـ lpApplicationName، ويصير argv[0] للعملية الابن الرمز في مقدّمة lpCommandLine. ما لم تُبقِ الاثنين متّسقين بالعرف، تنكسر الشيفرة التي تستخرج مسارها من argv[0]. الطريقة الموثوقة للحصول على مسارك هي GetModuleFileNameW.4

كيف تُقرَّر الوحدة المنفَّذة وargv[0]عندما يُمرَّر lpApplicationName وlpCommandLine معاً تُقرَّر الوحدة التي تعمل بـ lpApplicationName ويكون argv[0] للابن الرمز في مقدّمة lpCommandLine. تنكسر الشيفرة التي تستخرج مسارها من argv[0] عندما يتباعد الاثنان، لذا خذ مسارك بـ GetModuleFileNameWتنكسر عندما يتباعداناستخدم بدلاً منهlpApplicationNameالوحدة التي تعملالرمز في مقدّمة lpCommandLineargv[0] للابنشيفرة تستخرج مسارها من argv[0]GetModuleFileNameW

الشكل 7: «ما يعمل» و«ما يدخل argv[0]» يُقرَّران على حدة. تصميم يستخرج مساره من argv[0] لا يقوم فوق هذا الفصل.

نقطة أخرى: عندما يكون lpApplicationName هو NULL يُقيَّد جزء اسم الملفّ التنفيذي من lpCommandLine بـ MAX_PATH.1 لمعاملة المسارات الطويلة انظر «MAX_PATH ومطبّات المسارات وأسماء الملفّات في Windows».

6. قواعد جهة التجميع ── تكفي دالّة واحدة

ما إن تعرف قواعد التقسيم حتّى تستطيع بناء «سلسلة سيقسّمها الطرف الآخر إلى الأصل» بتشغيلها عكسياً فحسب. لكلّ وسيط من argv[1] فما بعده افعل ما يلي.6

  1. إن لم يكن فارغاً ولم يحتوِ مسافات ولا علامات اقتباس، ضعه كما هو.
  2. وإلّا فأحِط الكلّ بعلامات اقتباس. داخل الإحاطة،
    • حوّل تتالياً من k من الـ backslash مباشرة قبل علامة اقتباس إلى 2k+1 ثم ضع علامة الاقتباس (جعل العدد فردياً يحوّله إلى «علامة اقتباس حرفية»)؛
    • حوّل تتالياً من k من الـ backslash الختامي إلى 2k (تسبق علامة الإغلاق، فعدد زوجي يحوّله إلى «نهاية الإحاطة»)؛
    • اترك كلّ backslash آخر كما هو.
  3. ضع سلسلة فارغة كـ "".
تدفّق القرار لتجميع وسيط واحدإن كان الوسيط غير فارغ ولا يحتوي مسافات ولا علامات اقتباس فضعه كما هو؛ وإلّا فأحِطه بعلامات اقتباس، واجعل الـ backslash قبل علامة اقتباس 2k+1 والـ backslash الختامي 2k، وقدّم علامات الاقتباس بـ backslash، ثم أغلقلانعمتلقَّ وسيطاً واحداًفارغ، أو يحتوي مسافة أو علامة اقتباس؟ضعه كما هوعلامة اقتباس افتتاحامسح من اليسارk من الـ backslash قبل علامة اقتباس ← 2k+1k من الـ backslash الختامي ← 2kكلّ شيء آخر كما هوعلامة اقتباس إغلاق

الشكل 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

أين يصير ArgumentList وArguments سلسلةيهرب .NET عناصر ArgumentList عنصراً عنصراً ويجمّعها سلسلة واحدة ثم يمرّرها إلى CreateProcess، بينما تمرّر Arguments السلسلة التي جمّعها المستدعي كما هي. كلاهما سلسلة واحدة عند وصوله إلى نظام التشغيلArgumentList (عنصر واحد = وسيط واحد)يهرب .NET كلّ عنصر ويربطArguments (سلسلة واحدة جمّعتها بنفسك)كما هيسلسلة سطر أوامر واحدة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\ صحيحاً.

اختيار وسيلة التجميع حسب إصدار .NETعلى .NET Core 2.1 فما بعده أَوكِل الأمر إلى ProcessStartInfo.ArgumentList؛ وعلى .NET Framework جمّع سلسلة Arguments بدالّتك وفق القواعد نفسها. في الحالين لا تكتب علامات اقتباس يدوياً بربط سلاسلCore 2.1 فما بعدهFrameworkما إصدار .NET؟أضف إلى ArgumentList عنصراً عنصراًجمّع Arguments بدالّتكلا تكتب علامات اقتباس يدوياً

الشكل 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);                    // نسيان هذا يسرّب مقبضاً واحداً لكلّ تشغيل
تقسيم الأدوار بين المعاملين الممرَّرين إلى CreateProcessWيثبّت lpApplicationName الوحدة المراد تنفيذها، ويقرّر lpCommandLine السلسلة التي تتلقّاها العملية الابن عبر GetCommandLineW. مرّر lpCommandLine مخزناً قابلاً للكتابة وأبقِ argv[0] في المقدّمة متّسقاً مع lpApplicationNameأبقِ متّسقاًlpApplicationName: المسار الكامل للملفّ التنفيذيتُثبَّت الوحدة المراد تنفيذهاlpCommandLine: مخزن قابل للكتابةالسلسلة التي يتلقّاها الابن عبر GetCommandLineWالرمز في المقدّمة = argv[0]الباقي = وسائط مجمَّعة بقواعد الفصل 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.

توسّط cmd.exe يزيد مراحل التفسيرتشغيل الـ exe المستهدف مباشرة يعني أن التقسيم يحدث مرّة واحدة في محلّل الهدف، لكن المرور عبر cmd.exe /c يضيف تفسير نحو cmd.exe، والملفّ الدفعي فوق ذلك يتلقّى السلسلة الخام، فتتغيّر قواعد الإحاطة في كلّ مرحلةعمليتك ← الـ exe المستهدفتقسيم مرّة واحدة، بمحلّل الهدف فقطعمليتك ← cmd.exe /c ← الـ exe المستهدفيُضاف تفسير نحو cmd.exe (ampersand والأنبوب والأقواس وعلامة الإقحام)التقسيم بمحلّل الهدفعمليتك ← cmd.exe /c ← ملفّ دفعييتلقّى الملفّ الدفعي السلسلة الخامتمرير قيم غير موثوقة عبره يصير حقن أوامر

الشكل 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 جاهزاً لا تستطيع تعديله، الخيار الوحيد تقسيم الاستدعاءات بحيث يستوعب كلّ منها الحدّ.

حدود تمرير قيم متغيّرة الطول وسائط، والطريق حولهاتصفيف قيم متغيّرة الطول مثل قائمة ملفّات وسائط يبلغ حدّ cmd.exe البالغ 8191 حرفاً أو حدّ CreateProcess البالغ 32767 وحدة ترميز UTF-16 مع نموّ العدد. إن استطاع الهدف قراءة ملفّ استجابة (أو أمكن إصلاحه) فانتقل إلى طريقة ملفّ الاستجابة بكتابة القيم في ملفّ وتمرير المسار فقط؛ إن كان الهدف exe جاهزاً لا يقدر فقسّم الاستدعاءاتيستطيع الهدف قراءة ملفّ استجابةexe جاهز لا يقدرصفّ قيماً متغيّرة الطول (قائمة ملفّات إلخ) وسائطتنمو السلسلة مع نموّ العدديُبلَغ الحدّ (cmd.exe 8,191 / CreateProcess 32,767)في يوم يفشل التشغيل فجأةاكتب القيم في ملفّ ومرّر المسار فقط (ملفّ استجابة)قسّم الاستدعاءات

الشكل 13: الحدّ من نوع المشكلات «سليمة اليوم». للوسائط التي تنمو بنسبة العدد، إن استطاع الهدف قراءة ملفّ استجابة (أو أمكن إصلاحه) فافعل ذلك من البداية.

11. تحقّق ممّا وصل فعلاً

قبل زيادة الإحاطة بالتخمين، أقصر طريق النظر إلى الوسائط التي وصلت إلى الهدف. ثلاثة أمور تُنظَر: «السلسلة المجمَّعة في جهة الاستدعاء»، و«السلسلة التي وصلت إلى جهة الهدف»، و«المصفوفة بعد التقسيم»، وأربع وسائل لذلك. قبل ذلك وعد واحد. أيّاً كانت الوسيلة، اخفِ الأسرار قبل تسجيل سطر أوامر في سجلّ. إن وضع التصميم كلمات مرور أو مفاتيح API أو رموزاً في الوسائط، كتابتها كما هي تترك الأسرار في السجلّ، سواء كان سجلّ المستدعي أو سجلّ بدء الهدف. تُحفَظ السجلّات أطول من العملية ويراها أناس أكثر. في الأصل يمكن لعمليات أخرى على الجهاز نفسه قراءة سطر أوامر، كما مع Process Explorer المذكور لاحقاً، لذا التدبير الجذري تصميم يمرّر كلمات المرور والرموز لا وسائط بل عبر مسار آخر مثل الدخل القياسي أو مخزن إعدادات محمي؛ والإخفاء في السجلّات احتياط فوق ذلك. إمّا فسّر الوسائط بعد التقسيم (في جهة الاستدعاء، العناصر قبل التجميع) وأخفِ قيم الخيارات التي قد تكون سرّاً قبل التسجيل، أو فعّل تسجيل السلسلة الخام فقط في وضع تشخيص مقيَّد.

  1. في جهة الاستدعاء سجّل السلسلة التي جمّعتها. هذا هو lpCommandLine مباشرة قبل تمريره إلى CreateProcess. تفترض هذه المقارنة تشغيلاً بـ UseShellExecute = false أو استدعاء مباشراً لـ CreateProcess. عندما تفتح مستنداً أو عنوان URL بـ UseShellExecute = true يبني ارتباط الملفّ سطر الأوامر الفعلي عبر ShellExecuteEx (الفصل 7)، لذا تختلف سلسلة المستدعي وسلسلة الهدف حتّى بلا cmd.exe أو ملفّ دفعي، وليست تلك مشكلة الفصل 9. إن استخدمت ArgumentList في .NET فلا تصلح قائمة العناصر كما هي للمقارنة. العناصر القيم قبل الإحاطة وقبل مضاعفة الـ backslash الختامي، وما يصل إلى نظام التشغيل السلسلة التي نسّقها .NET منها. إمّا أعد بناء سلسلة واحدة من العناصر بالقواعد نفسها لـ BuildArguments في الفصل 7 وسجّل ذلك (يعطي النتيجة نفسها للتنسيق الذي يفعله ArgumentList داخلياً)، أو قارن قائمة العناصر مباشرة بالمصفوفة بعد التقسيم. هذه الوسيلة الوحيدة لرؤية «المخزن الأصلي للمستدعي»؛ أمّا Process Explorer وسجلّ الهدف المذكوران أدناه فلا يظهران سوى السلسلة التي أعاد بناءها طور cmd.exe أو دفعة بينهما. عند التسجيل التزم الوعد من المقدّمة وأخفِ قيم العناصر التي قد تكون سرّاً (عنصر مخفي لم يعد يطابق سلسلة الهدف، لذا استثنِ ذلك العنصر من المقارنة).
  2. جهّز 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
  1. انظر إلى سطر أوامر العملية الابن في Process Explorer. تُظهر خصائص العملية سلسلة سطر الأوامر التي تحملها العملية الابن. هذه وسيلة لفحص «السلسلة التي وصلت إلى جهة الهدف»؛ ولا تخبرك «المصفوفة بعد التقسيم». المعروض السلسلة المحفوظة في جهة العملية الابن، لذا كما لُمس في الفصل 2 قد يكون نظام التشغيل قد كمّل المسار الكامل لاسم الملفّ التنفيذي في المقدّمة، وإن توسّط cmd.exe أو ملفّ دفعي فما تراه السلسلة التي أعاد cmd.exe بناءها. النقاط الرئيسة ألّا تفزع من فرق في الرمز في المقدّمة وحده، وأن سلسلة المستدعي الأصلية لا تُعرَف إلّا من السجلّ في البند 1. الاستخدام مشمول في «Process Explorer / Handle / VMMap عملياً».
  2. عند بدء تطبيقك سجّل سطر الأوامر الذي تلقّاه. عندما يقول أحد في الميدان «لا يبدأ»، وجود سجلّ للسلسلة التي بدأ بها يتيح لك عزل ما إذا كانت مشكلة وسائط أوّلاً. هنا أيضاً لا تحفظ قيمة عودة GetCommandLineW كما هي. التزم الوعد من المقدّمة: إمّا فسّر الوسائط بعد التقسيم وأخفِ قيماً قد تكون سرّاً قبل التسجيل، أو فعّل تسجيل السلسلة الخام فقط في وضع تشخيص مقيَّد.

ترتيب المقارنة كما يلي. قارن أوّلاً سلسلة المستدعي (البند 1) بسلسلة الهدف (البند 3 أو 4). إن لم تتطابقا بمعزل عن اسم الملفّ التنفيذي في المقدّمة فقد حوّلها طور بينهما: cmd.exe أو ملفّ دفعي لتشغيل مباشر (الفصل 9)، أو ارتباط ملفّ الصدفة لـ UseShellExecute = true (الفصل 7). استبدال شيفرتك بدالّة الفصل 6 لن يصلح ذلك. إن تطابقتا فقارن تلك السلسلة بالمصفوفة بعد التقسيم (البند 2). إن قُسِّمت وفق القواعد لكنّها ليست المصفوفة التي تريد فالمشكلة في جهة التجميع؛ وإن لم تُقسَّم وفق القواعد فالمشكلة محلّل المستقبل.

ترتيب عزل مشكلة وسائطقارن أوّلاً سجلّ السلسلة المجمَّعة في جهة الاستدعاء بسلسلة الهدف كما تُرى في Process Explorer أو سجلّ بدء الهدف. إن لم تتطابقا بمعزل عن اسم الملفّ التنفيذي في المقدّمة فقد حوّلها طور بينهما (cmd.exe أو ملفّ دفعي لتشغيل مباشر، ارتباط ملفّ الصدفة لـ UseShellExecute=true). إن تطابقتا فقارن بالمصفوفة بعد التقسيم؛ إن قُسِّمت بالقواعد لكنّها ليست المصفوفة التي تريد فالمشكلة في جهة التجميع، وإن لم تُقسَّم بالقواعد فالمشكلة محلّل المستقبللانعمنعم: قُسِّمت، لكن ليست المصفوفة التي تريدلا: لم تُقسَّم بالقواعدالوسائط خاطئةانظر إلى السلسلة المجمَّعة في جهة الاستدعاء (سجلّ المستدعي)انظر إلى سلسلة الهدف (Process Explorer / سجلّ بدء الهدف)هل تتطابقان بمعزل عن اسم الملفّ التنفيذي في المقدّمة؟حوّلها طور بينهما (انظر الفصلين 9 و7)انظر إلى المصفوفة بعد التقسيم (exe عرض على وقت التشغيل نفسه للهدف)هل تتقابل السلسلة والمصفوفة بالقواعد؟مشكلة جهة تجميع: استبدل بدالّة الفصل 6مشكلة في محلّل المستقبل

الشكل 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 ختامي».

خمسة وعود تمنع أعطال الوسائطمرّر المسار الكامل للملفّ التنفيذي في lpApplicationName وأحِط الرمز في المقدّمة بعلامات اقتباس أيضاً، وأَوكِل الإحاطة إلى دالّة تتبع القواعد أو إلى ArgumentList، ولا تولّد صيغة علامات اقتباس متجاورة داخل وسيط غير فارغ، ولا تمرّر قيماً من الخارج عبر cmd.exe أو ملفّ دفعي، واستخدم ملفّ استجابة للوسائط التي تنمو مع العدد فقط عندما يستطيع الهدف قراءة واحد. على فرض أن الهدف يفسّر بقواعد التقسيم المنشورة ولم يفعّل توسيع أحرف البدل، تمنع هذه النقاط الخمس الأعطال الناجمة عن مسارات فيها مسافات وbackslash ختاميمرّر المسار الكامل في lpApplicationName وأحِط الرمز في المقدّمة أيضاًأَوكِل الإحاطة إلى دالّة تتبع القواعد أو إلى ArgumentListلا تولّد علامات اقتباس متجاورة داخل الإحاطةلا تمرّر قيماً من الخارج عبر cmd.exe أو ملفّ دفعياستخدم ملفّ استجابة للوسائط النامية (عندما يستطيع الهدف القراءة)لا أعطال من المسافات أو الـ backslash الختامي

الشكل 15: كلّ من الوعود الخمسة إعادة صياغة لـ «ثبّت الوحدة المراد تنفيذها، ومرّر فقط سلاسل يستطيع محلّل الهدف تقسيمها». الفرضية أن الهدف يفسّر بقواعد التقسيم المنشورة ولم يفعّل توسيع أحرف البدل (الفصلان 6 و8)؛ وفوق ذلك تمنع هذه النقاط الخمس الأعطال الناجمة عن المسافات والـ backslash الختامي.

عندما لا يعمل الأمر، قبل زيادة الهروب بالتخمين، انظر إلى الأمور الثلاثة: السلسلة المجمَّعة في جهة الاستدعاء، والسلسلة التي وصلت إلى جهة الهدف، والمصفوفة بعد التقسيم. إن اختلفت سلسلتا المستدعي والهدف فالمسؤول طور بينهما (cmd.exe أو ملفّ دفعي، أو ارتباط ملفّ الصدفة لـ UseShellExecute = true)؛ وإن كانتا سواء فتقابل السلسلة والمصفوفة يقرّر ما إذا كانت جهة التجميع أو جهة الاستقبال.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت ذ.م.م. مع تصميم تطبيقات Windows التي تجمع أدوات خارجية وEXE داخلية، وتحقيق السبب الجذري لتشغيل عمليات ابنة «تبدأ في بعض البيئات ولا تبدأ في أخرى»، ومراجعة شيفرة تشغيل العمليات ضمن الترحيل من .NET Framework إلى .NET. نرحّب بالاستشارة حتّى من حالة واحدة لـ «تتشوّه الوسائط».

روابط مرجعيّة

  1. 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

  2. Microsoft Learn, GetCommandLineW function (processenv.h). حول إعادتها سلسلة سطر أوامر العملية الحالية، وعدم تحرير قيمة العودة أو تعديلها، وإمكان تحويلها إلى صيغة argv عبر CommandLineToArgvW، وإمكان عدم تطابقها مع السلسلة التي مرّرها الأب إلى CreateProcess لأن نظام التشغيل يكمّل المسار الكامل لاسم الملفّ التنفيذي.  2

  3. Microsoft Learn, CommandLineToArgvW function (shellapi.h). حول المعاملة الخاصّة للـ backslash مباشرة قبل علامة اقتباس مزدوجة (2n تعطي n مع فتح الإحاطة أو إغلاقها، و2n+1 تعطي n مع علامة اقتباس حرفية، وتبقى كما هي عندما لا تلي علامة اقتباس)، وصيرورة المسافات جزءاً من الوسيط في وضع «داخل علامات الاقتباس»، وجواز اسم البرنامج في المقدّمة بعلامات اقتباس أو بدونها، وصيرورة الوسيط الأوّل سلسلة فارغة عندما يبدأ lpCmdLine بمسافة، وإعادة مسار الملفّ التنفيذي الحالي عندما تُمرَّر سلسلة فارغة، وتحرير قيمة العودة بـ LocalFree واحد.  2 3 4 5

  4. Microsoft Learn, main function 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

  5. Microsoft Learn, ProcessStartInfo.ArgumentList Property. حول عدم حاجة السلاسل المضافة إلى هروب مسبقاً، واستقلال ArgumentList وArguments وعدم إمكان استخدامهما معاً، وهروب ArgumentList للوسائط وتجميعه سلسلة واحدة داخلياً تُمرَّر إلى نظام التشغيل عند Process.Start، وكون ArgumentList الاختيار إن لم تكن واثقاً من الإحاطة، وخطر جمعه مع بيانات غير موثوقة، وسريانه على .NET Core 2.1 فما بعده.  2 3 4

  6. dotnet/runtime (GitHub), PasteArguments.cs وPasteArguments.Windows.cs. شيفرة التجميع المستخدمة داخل ArgumentList. حول وضع وسيط غير فارغ بلا مسافات ولا علامات اقتباس كما هو، وإلّا إحاطته بعلامات اقتباس، ومضاعفة الـ backslash الختامي، وجعل الـ backslash قبل علامة اقتباس مضاعفاً زائداً واحداً، وتقديم علامات الاقتباس دائماً بـ backslash، وعدم توليد صيغة علامة اقتباس تلي علامة إغلاق لأن VC قبل 2008 وبعده يفسّرانها على نحو مختلف، ولـ argv[0] الإحاطة بعلامات اقتباس فقط إن وُجدت مسافات والإلقاء إن احتوى علامة اقتباس.  2 3 4 5 6

  7. Microsoft Learn, about_Parsing. حول وصول وسائط الملفّ الدفعي إلى cmd.exe كسلسلة سطر أوامر خام، والتحذير من تمرير دخل غير موثوق.  2

  8. Microsoft Learn, Command prompt (Cmd.exe) command-line string limitation. حول كون أقصى طول سلسلة في موجه الأوامر 8,191 حرفاً، وسريانه على أسطر الأوامر داخل ملفّ دفعي أيضاً، وطريقة التحايل بكتابة الوسائط في ملفّ وتمرير اسم ذلك الملفّ.  2 3

  9. Microsoft Learn, WinMain function (winbase.h). حول كون lpCmdLine سطر الأوامر بلا اسم البرنامج، والحصول على سطر الأوامر كلّه بـ GetCommandLine، ووجود wWinMain كنقطة دخول Unicode. 

  10. dotnet/runtime (GitHub), apphost.c وdotnet.cpp. حول كون نقطتَي دخول apphost وdotnet.exe هما wmain(int argc, wchar_t* argv[]) على Windows وتمرير argv الذي بناه وقت تشغيل C مباشرة إلى معالجة بدء المضيف. 

  11. dotnet/runtime (GitHub), corhost.cpp. حول بناء ExecuteAssembly مصفوفة Environment.GetCommandLineArgs() بـ SetCommandLineArgs(pwzAssemblyPath, argc, argv)، وكون العنصر الأوّل اسم التشغيل الممرَّر من المضيف (أو مسار التجميع إن لم يوجد) ويليه argv، ووصول ذلك argv فقط إلى Main 2

  12. dotnet/runtime (GitHub), Environment.cs وEnvironment.Windows.cs. حول إعادة GetCommandLineArgs المصفوفة المهيَّأة عند البدء (s_commandLineArgs)، وسقوط مكتبة مستضافة بلا ذلك إلى تقسيم قيمة عودة GetCommandLineW بـ SegmentCommandLine الخاص بوقت التشغيل، واتّباع تلك القواعد وثائق دالّة main في MSVC، وعدم استخدام CommandLineToArgvW لأن سلوكه يختلف قليلاً.  2 3

  13. Microsoft Learn, Main() and command-line arguments. حول عدم صيرورة args في Main null أبداً، وخلافاً لـ C/C++ عدم تضمين اسم البرنامج في بداية args بل كونه العنصر الأوّل لـ GetCommandLineArgs()

  14. Microsoft Learn, dotnet command. حول كون تشغيل التطبيق بالشكل dotnet [خيارات وقت التشغيل] <مسار التطبيق> [وسائط]، والوسائط بعد مسار التطبيق هي ما يصل إلى التطبيق. 

  15. Microsoft Learn, Environment.GetCommandLineArgs Method. حول كون العنصر الأوّل اسم الملفّ التنفيذي، وفصل الوسائط بمسافات مع سماح علامات الاقتباس المزدوجة بمسافات داخلها، وعدم امتلاك علامات الاقتباس المفردة تلك الوظيفة، وقواعد الأعداد الزوجية والفردية من الـ backslash وعلامات الاقتباس، وجدول الدخل والنتائج. 

  16. Microsoft Learn, ProcessStartInfo.Arguments Property. حول كون طول السلسلة أقلّ من 32,699، وتفسير التطبيق المستهدف للوسائط لذا يجب أن تطابق توقّعاته، وعدم تمرير علامات الاقتباس نفسها إلى الهدف عندما يُحاط وسيط يحتوي مسافات، واستقلاله عن ArgumentList 2

  17. Microsoft Learn, cmd. حول كون & و| و( ) أحرفاً خاصّة تحتاج ^ أو علامات اقتباس، وقائمة الأحرف الخاصّة التي ينبغي إحاطتها، وشروط الاحتفاظ بعلامات الاقتباس عند تحديد /c /k (عدم استخدام /s، وزوج واحد من علامات الاقتباس، وعدم احتواء أحرف خاصّة، واحتواء مسافات، وكونه اسم ملفّ تنفيذي) وطريقة نزع علامة الاقتباس الأولى عندما لا تُستوفى الشروط. 

  18. 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 وتجعل الدفعة وسيطه. 

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

ألا توجد في 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 تفسير الرموز.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة